diff options
Diffstat (limited to 'internal/db')
24 files changed, 1448 insertions, 341 deletions
diff --git a/internal/db/db.go b/internal/db/db.go index 4921270e7..69ad7b822 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -27,8 +27,7 @@ import (  	"github.com/go-fed/activity/pub"  	"github.com/sirupsen/logrus"  	"github.com/superseriousbusiness/gotosocial/internal/config" -	"github.com/superseriousbusiness/gotosocial/internal/db/model" -	"github.com/superseriousbusiness/gotosocial/pkg/mastotypes" +	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"  )  const dbTypePostgres string = "POSTGRES" @@ -79,6 +78,11 @@ type DB interface {  	// In case of no entries, a 'no entries' error will be returned  	GetWhere(key string, value interface{}, i interface{}) error +	// // GetWhereMany gets one entry where key = value for *ALL* parameters passed as "where". +	// // That is, if you pass 2 'where' entries, with 1 being Key username and Value test, and the second +	// // being Key domain and Value example.org, only entries will be returned where BOTH conditions are true. +	// GetWhereMany(i interface{}, where ...model.Where) 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 @@ -88,6 +92,11 @@ type DB interface {  	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.  	Put(i interface{}) error +	// Upsert stores or updates i based on the given conflict column, as in https://www.postgresqltutorial.com/postgresql-upsert/ +	// It is up to the implementation to figure out how to store it, and using what key. +	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice. +	Upsert(i interface{}, conflictColumn string) error +  	// UpdateByID updates i with id id.  	// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.  	UpdateByID(id string, i interface{}) error @@ -107,41 +116,46 @@ type DB interface {  		HANDY SHORTCUTS  	*/ +	// CreateInstanceAccount creates an account in the database with the same username as the instance host value. +	// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. +	// This is needed for things like serving files that belong to the instance and not an individual user/account. +	CreateInstanceAccount() error +  	// GetAccountByUserID is a shortcut for the common action of fetching an account corresponding to a user ID.  	// The given account pointer will be set to the result of the query, whatever it is.  	// In case of no entries, a 'no entries' error will be returned -	GetAccountByUserID(userID string, account *model.Account) error +	GetAccountByUserID(userID string, account *gtsmodel.Account) error  	// GetFollowRequestsForAccountID is a shortcut for the common action of fetching a list of follow requests targeting the given account ID.  	// The given slice 'followRequests' will be set to the result of the query, whatever it is.  	// In case of no entries, a 'no entries' error will be returned -	GetFollowRequestsForAccountID(accountID string, followRequests *[]model.FollowRequest) error +	GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error  	// GetFollowingByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is following.  	// The given slice 'following' will be set to the result of the query, whatever it is.  	// In case of no entries, a 'no entries' error will be returned -	GetFollowingByAccountID(accountID string, following *[]model.Follow) error +	GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error  	// GetFollowersByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is followed by.  	// The given slice 'followers' will be set to the result of the query, whatever it is.  	// In case of no entries, a 'no entries' error will be returned -	GetFollowersByAccountID(accountID string, followers *[]model.Follow) error +	GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error  	// GetStatusesByAccountID is a shortcut for the common action of fetching a list of statuses produced by accountID.  	// The given slice 'statuses' will be set to the result of the query, whatever it is.  	// In case of no entries, a 'no entries' error will be returned -	GetStatusesByAccountID(accountID string, statuses *[]model.Status) error +	GetStatusesByAccountID(accountID string, statuses *[]gtsmodel.Status) error  	// GetStatusesByTimeDescending 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 -	GetStatusesByTimeDescending(accountID string, statuses *[]model.Status, limit int) error +	GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int) error  	// GetLastStatusForAccountID simply gets the most recent status by the given account.  	// The given slice 'status' pointer will be set to the result of the query, whatever it is.  	// In case of no entries, a 'no entries' error will be returned -	GetLastStatusForAccountID(accountID string, status *model.Status) error +	GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error  	// IsUsernameAvailable checks whether a given username is available on our domain.  	// Returns an error if the username is already taken, or something went wrong in the db. @@ -156,32 +170,112 @@ type DB interface {  	// NewSignup creates a new user in the database with the given parameters, with an *unconfirmed* email address.  	// 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) (*model.User, error) +	NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*gtsmodel.User, error)  	// SetHeaderOrAvatarForAccountID sets the header or avatar for the given accountID to the given media attachment. -	SetHeaderOrAvatarForAccountID(mediaAttachment *model.MediaAttachment, accountID string) error +	SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error  	// GetHeaderAvatarForAccountID gets the current avatar for the given account ID.  	// The passed mediaAttachment pointer will be populated with the value of the avatar, if it exists. -	GetAvatarForAccountID(avatar *model.MediaAttachment, accountID string) error +	GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error  	// GetHeaderForAccountID gets the current header for the given account ID.  	// The passed mediaAttachment pointer will be populated with the value of the header, if it exists. -	GetHeaderForAccountID(header *model.MediaAttachment, accountID string) error +	GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error + +	// Blocked checks whether a block exists in eiher direction between two accounts. +	// That is, it returns true if account1 blocks account2, OR if account2 blocks account1. +	Blocked(account1 string, account2 string) (bool, error) + +	// StatusVisible returns true if targetStatus is visible to requestingAccount, based on the +	// privacy settings of the status, and any blocks/mutes that might exist between the two accounts +	// or account domains. +	// +	// StatusVisible will also check through the given slice of 'otherRelevantAccounts', which should include: +	// +	// 1. Accounts mentioned in the targetStatus +	// +	// 2. Accounts replied to by the target status +	// +	// 3. Accounts boosted by the target status +	// +	// Will return an error if something goes wrong while pulling stuff out of the database. +	StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) + +	// Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out. +	Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) + +	// Mutuals returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out. +	Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) + +	// PullRelevantAccountsFromStatus returns all accounts mentioned in a status, replied to by a status, or boosted by a status +	PullRelevantAccountsFromStatus(status *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error) + +	// GetReplyCountForStatus returns the amount of replies recorded for a status, or an error if something goes wrong +	GetReplyCountForStatus(status *gtsmodel.Status) (int, error) + +	// GetReblogCountForStatus returns the amount of reblogs/boosts recorded for a status, or an error if something goes wrong +	GetReblogCountForStatus(status *gtsmodel.Status) (int, error) + +	// GetFaveCountForStatus returns the amount of faves/likes recorded for a status, or an error if something goes wrong +	GetFaveCountForStatus(status *gtsmodel.Status) (int, error) + +	// StatusFavedBy checks if a given status has been faved by a given account ID +	StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) + +	// StatusRebloggedBy checks if a given status has been reblogged/boosted by a given account ID +	StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) + +	// StatusMutedBy checks if a given status has been muted by a given account ID +	StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) + +	// StatusBookmarkedBy checks if a given status has been bookmarked by a given account ID +	StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) + +	// StatusPinnedBy checks if a given status has been pinned by a given account ID +	StatusPinnedBy(status *gtsmodel.Status, accountID string) (bool, error) + +	// FaveStatus faves the given status, using accountID as the faver. +	// The returned fave will be nil if the status was already faved. +	FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) + +	// UnfaveStatus unfaves the given status, using accountID as the unfaver (sure, that's a word). +	// The returned fave will be nil if the status was already not faved. +	UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) + +	// WhoFavedStatus returns a slice of accounts who faved the given status. +	// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. +	WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error)  	/*  		USEFUL CONVERSION FUNCTIONS  	*/ -	// AccountToMastoSensitive takes a db model account as a param, and returns a populated mastotype account, or an error -	// if something goes wrong. The returned account should be ready to serialize on an API level, and may have sensitive fields, -	// so serve it only to an authorized user who should have permission to see it. -	AccountToMastoSensitive(account *model.Account) (*mastotypes.Account, error) - -	// AccountToMastoPublic takes a db model account as a param, and returns a populated mastotype account, or an error -	// if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields. -	// In other words, this is the public record that the server has of an account. -	AccountToMastoPublic(account *model.Account) (*mastotypes.Account, error) +	// MentionStringsToMentions takes a slice of deduplicated, lowercase account names in the form "@test@whatever.example.org" for a remote account, +	// or @test for a local account, which have been mentioned in a status. +	// It takes the id of the account that wrote the status, and the id of the status itself, and then +	// checks in the database for the mentioned accounts, and returns a slice of mentions generated based on the given parameters. +	// +	// Note: this func doesn't/shouldn't do any manipulation of the accounts in the DB, it's just for checking +	// if they exist in the db and conveniently returning them if they do. +	MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) + +	// TagStringsToTags takes a slice of deduplicated, lowercase tags in the form "somehashtag", which have been +	// used in a status. It takes the id of the account that wrote the status, and the id of the status itself, and then +	// returns a slice of *model.Tag corresponding to the given tags. If the tag already exists in database, that tag +	// will be returned. Otherwise a pointer to a new tag struct will be created and returned. +	// +	// Note: this func doesn't/shouldn't do any manipulation of the tags in the DB, it's just for checking +	// if they exist in the db already, and conveniently returning them, or creating new tag structs. +	TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) + +	// EmojiStringsToEmojis takes a slice of deduplicated, lowercase emojis in the form ":emojiname:", which have been +	// used in a status. It takes the id of the account that wrote the status, and the id of the status itself, and then +	// returns a slice of *model.Emoji corresponding to the given emojis. +	// +	// Note: this func doesn't/shouldn't do any manipulation of the emoji in the DB, it's just for checking +	// if they exist in the db and conveniently returning them if they do. +	EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error)  }  // New returns a new database service that satisfies the DB interface and, by extension, diff --git a/internal/db/model/README.md b/internal/db/gtsmodel/README.md index 12a05ddec..12a05ddec 100644 --- a/internal/db/model/README.md +++ b/internal/db/gtsmodel/README.md diff --git a/internal/db/model/account.go b/internal/db/gtsmodel/account.go index 70ee92929..4bf5a9d33 100644 --- a/internal/db/model/account.go +++ b/internal/db/gtsmodel/account.go @@ -16,15 +16,14 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -// Package model contains types used *internally* by GoToSocial and added/removed/selected from the database. +// Package gtsmodel contains types used *internally* by GoToSocial and added/removed/selected from the database.  // These types should never be serialized and/or sent out via public APIs, as they contain sensitive information.  // The annotation used on these structs is for handling them via the go-pg ORM (hence why they're in this db subdir).  // See here for more info on go-pg model annotations: https://pg.uptrace.dev/models/ -package model +package gtsmodel  import (  	"crypto/rsa" -	"net/url"  	"time"  ) @@ -38,33 +37,17 @@ type Account struct {  	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`  	// Username of the account, should just be a string of [a-z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org``  	Username string `pg:",notnull,unique:userdomain"` // username and domain should be unique *with* each other -	// Domain of the account, will be empty if this is a local account, otherwise something like ``example.org`` or ``mastodon.social``. Should be unique with username. +	// Domain of the account, will be null if this is a local account, otherwise something like ``example.org`` or ``mastodon.social``. Should be unique with username.  	Domain string `pg:",unique:userdomain"` // username and domain should be unique *with* each other  	/*  		ACCOUNT METADATA  	*/ -	// File name of the avatar on local storage -	AvatarFileName string -	// Gif? png? jpeg? -	AvatarContentType string -	// Size of the avatar in bytes -	AvatarFileSize int -	// When was the avatar last updated? -	AvatarUpdatedAt time.Time `pg:"type:timestamp"` -	// Where can the avatar be retrieved? -	AvatarRemoteURL *url.URL `pg:"type:text"` -	// File name of the header on local storage -	HeaderFileName string -	// Gif? png? jpeg? -	HeaderContentType string -	// Size of the header in bytes -	HeaderFileSize int -	// When was the header last updated? -	HeaderUpdatedAt time.Time `pg:"type:timestamp"` -	// Where can the header be retrieved? -	HeaderRemoteURL *url.URL `pg:"type:text"` +	// ID of the avatar as a media attachment +	AvatarMediaAttachmentID string +	// ID of the header as a media attachment +	HeaderMediaAttachmentID string  	// DisplayName for this account. Can be empty, then just the Username will be used for display purposes.  	DisplayName string  	// a key/value map of fields that this account has added to their profile @@ -74,13 +57,11 @@ type Account struct {  	// Is this a memorial account, ie., has the user passed away?  	Memorial bool  	// This account has moved this account id in the database -	MovedToAccountID int +	MovedToAccountID string  	// When was this account created?  	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`  	// When was this account last updated?  	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` -	// When should this account function until -	SubscriptionExpiresAt time.Time `pg:"type:timestamp"`  	// Does this account identify itself as a bot?  	Bot bool  	// What reason was given for signing up when this account was created? @@ -95,7 +76,7 @@ type Account struct {  	// Should this account be shown in the instance's profile directory?  	Discoverable bool  	// Default post privacy for this account -	Privacy string +	Privacy Visibility  	// Set posts from this account to sensitive by default?  	Sensitive bool  	// What language does this account post in? @@ -122,7 +103,7 @@ type Account struct {  	// URL for getting the featured collection list of this account  	FeaturedCollectionURL string `pg:",unique"`  	// What type of activitypub actor is this account? -	ActorType string +	ActorType ActivityStreamsActor  	// This account is associated with x account id  	AlsoKnownAs string @@ -130,7 +111,6 @@ type Account struct {  		CRYPTO FIELDS  	*/ -	Secret string  	// Privatekey for validating activitypub requests, will obviously only be defined for local accounts  	PrivateKey *rsa.PrivateKey  	// Publickey for encoding activitypub requests, will be defined for both local and remote accounts @@ -146,12 +126,10 @@ type Account struct {  	SilencedAt time.Time `pg:"type:timestamp"`  	// When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account)  	SuspendedAt time.Time `pg:"type:timestamp"` -	// How much do we trust this account 🤔 -	TrustLevel int  	// Should we hide this account's collections?  	HideCollections bool  	// id of the user that suspended this account through an admin action -	SuspensionOrigin int +	SuspensionOrigin string  }  // Field represents a key value field on an account, for things like pronouns, website, etc. diff --git a/internal/db/gtsmodel/activitystreams.go b/internal/db/gtsmodel/activitystreams.go new file mode 100644 index 000000000..059588a57 --- /dev/null +++ b/internal/db/gtsmodel/activitystreams.go @@ -0,0 +1,127 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel + +// ActivityStreamsObject refers to https://www.w3.org/TR/activitystreams-vocabulary/#object-types +type ActivityStreamsObject string + +const ( +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article +	ActivityStreamsArticle ActivityStreamsObject = "Article" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio +	ActivityStreamsAudio ActivityStreamsObject = "Audio" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document +	ActivityStreamsDocument ActivityStreamsObject = "Event" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event +	ActivityStreamsEvent ActivityStreamsObject = "Event" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image +	ActivityStreamsImage ActivityStreamsObject = "Image" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note +	ActivityStreamsNote ActivityStreamsObject = "Note" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page +	ActivityStreamsPage ActivityStreamsObject = "Page" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place +	ActivityStreamsPlace ActivityStreamsObject = "Place" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile +	ActivityStreamsProfile ActivityStreamsObject = "Profile" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship +	ActivityStreamsRelationship ActivityStreamsObject = "Relationship" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone +	ActivityStreamsTombstone ActivityStreamsObject = "Tombstone" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video +	ActivityStreamsVideo ActivityStreamsObject = "Video" +) + +// ActivityStreamsActor refers to https://www.w3.org/TR/activitystreams-vocabulary/#actor-types +type ActivityStreamsActor string + +const ( +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application +	ActivityStreamsApplication ActivityStreamsActor = "Application" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group +	ActivityStreamsGroup ActivityStreamsActor = "Group" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization +	ActivityStreamsOrganization ActivityStreamsActor = "Organization" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person +	ActivityStreamsPerson ActivityStreamsActor = "Person" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service +	ActivityStreamsService ActivityStreamsActor = "Service" +) + +// ActivityStreamsActivity refers to https://www.w3.org/TR/activitystreams-vocabulary/#activity-types +type ActivityStreamsActivity string + +const ( +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept +	ActivityStreamsAccept ActivityStreamsActivity = "Accept" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-add +	ActivityStreamsAdd ActivityStreamsActivity = "Add" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-announce +	ActivityStreamsAnnounce ActivityStreamsActivity = "Announce" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-arrive +	ActivityStreamsArrive ActivityStreamsActivity = "Arrive" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-block +	ActivityStreamsBlock ActivityStreamsActivity = "Block" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-create +	ActivityStreamsCreate ActivityStreamsActivity = "Create" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-delete +	ActivityStreamsDelete ActivityStreamsActivity = "Delete" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-dislike +	ActivityStreamsDislike ActivityStreamsActivity = "Dislike" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-flag +	ActivityStreamsFlag ActivityStreamsActivity = "Flag" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-follow +	ActivityStreamsFollow ActivityStreamsActivity = "Follow" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-ignore +	ActivityStreamsIgnore ActivityStreamsActivity = "Ignore" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-invite +	ActivityStreamsInvite ActivityStreamsActivity = "Invite" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-join +	ActivityStreamsJoin ActivityStreamsActivity = "Join" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-leave +	ActivityStreamsLeave ActivityStreamsActivity = "Leave" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-like +	ActivityStreamsLike ActivityStreamsActivity = "Like" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-listen +	ActivityStreamsListen ActivityStreamsActivity = "Listen" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move +	ActivityStreamsMove ActivityStreamsActivity = "Move" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-offer +	ActivityStreamsOffer ActivityStreamsActivity = "Offer" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-question +	ActivityStreamsQuestion ActivityStreamsActivity = "Question" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-reject +	ActivityStreamsReject ActivityStreamsActivity = "Reject" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-read +	ActivityStreamsRead ActivityStreamsActivity = "Read" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-remove +	ActivityStreamsRemove ActivityStreamsActivity = "Remove" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativereject +	ActivityStreamsTentativeReject ActivityStreamsActivity = "TentativeReject" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativeaccept +	ActivityStreamsTentativeAccept ActivityStreamsActivity = "TentativeAccept" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-travel +	ActivityStreamsTravel ActivityStreamsActivity = "Travel" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-undo +	ActivityStreamsUndo ActivityStreamsActivity = "Undo" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update +	ActivityStreamsUpdate ActivityStreamsActivity = "Update" +	// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-view +	ActivityStreamsView ActivityStreamsActivity = "View" +) diff --git a/internal/db/model/application.go b/internal/db/gtsmodel/application.go index c8eea6430..8e1398beb 100644 --- a/internal/db/model/application.go +++ b/internal/db/gtsmodel/application.go @@ -16,9 +16,7 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package model - -import "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" +package gtsmodel  // Application represents an application that can perform actions on behalf of a user.  // It is used to authorize tokens etc, and is associated with an oauth client id in the database. @@ -40,16 +38,3 @@ type Application struct {  	// a vapid key generated for this app when it was created  	VapidKey string  } - -// ToMasto returns this application as a mastodon api type, ready for serialization -func (a *Application) ToMasto() *mastotypes.Application { -	return &mastotypes.Application{ -		ID:           a.ID, -		Name:         a.Name, -		Website:      a.Website, -		RedirectURI:  a.RedirectURI, -		ClientID:     a.ClientID, -		ClientSecret: a.ClientSecret, -		VapidKey:     a.VapidKey, -	} -} diff --git a/internal/db/gtsmodel/block.go b/internal/db/gtsmodel/block.go new file mode 100644 index 000000000..fae43fbef --- /dev/null +++ b/internal/db/gtsmodel/block.go @@ -0,0 +1,19 @@ +package gtsmodel + +import "time" + +// Block refers to the blocking of one account by another. +type Block struct { +	// id of this block in the database +	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` +	// When was this block created +	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` +	// When was this block updated +	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` +	// Who created this block? +	AccountID string `pg:",notnull"` +	// Who is targeted by this block? +	TargetAccountID string `pg:",notnull"` +	// Activitypub URI for this block +	URI string +} diff --git a/internal/db/model/domainblock.go b/internal/db/gtsmodel/domainblock.go index e6e89bc20..dcfb2acee 100644 --- a/internal/db/model/domainblock.go +++ b/internal/db/gtsmodel/domainblock.go @@ -16,7 +16,7 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package model +package gtsmodel  import "time" diff --git a/internal/db/model/emaildomainblock.go b/internal/db/gtsmodel/emaildomainblock.go index 6610a2075..4cda68b02 100644 --- a/internal/db/model/emaildomainblock.go +++ b/internal/db/gtsmodel/emaildomainblock.go @@ -16,7 +16,7 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package model +package gtsmodel  import "time" diff --git a/internal/db/gtsmodel/emoji.go b/internal/db/gtsmodel/emoji.go new file mode 100644 index 000000000..da1e2e02c --- /dev/null +++ b/internal/db/gtsmodel/emoji.go @@ -0,0 +1,74 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel + +import "time" + +type Emoji struct { +	// database ID of this emoji +	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` +	// String shortcode for this emoji -- the part that's between colons. This should be lowercase a-z_ +	// eg., 'blob_hug' 'purple_heart' Must be unique with domain. +	Shortcode string `pg:",notnull,unique:shortcodedomain"` +	// Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis. +	Domain string `pg:",notnull,default:'',use_zero,unique:shortcodedomain"` +	// When was this emoji created. Must be unique with shortcode. +	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` +	// When was this emoji updated +	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` +	// Where can this emoji be retrieved remotely? Null for local emojis. +	// For remote emojis, it'll be something like: +	// https://hackers.town/system/custom_emojis/images/000/049/842/original/1b74481204feabfd.png +	ImageRemoteURL string +	// Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis. +	// For remote emojis, it'll be something like: +	// https://hackers.town/system/custom_emojis/images/000/049/842/static/1b74481204feabfd.png +	ImageStaticRemoteURL string +	// Where can this emoji be retrieved from the local server? Null for remote emojis. +	// Assuming our server is hosted at 'example.org', this will be something like: +	// 'https://example.org/fileserver/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/original/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' +	ImageURL string +	// Where can a static version of this emoji be retrieved from the local server? Null for remote emojis. +	// Assuming our server is hosted at 'example.org', this will be something like: +	// 'https://example.org/fileserver/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/small/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' +	ImageStaticURL string +	// Path of the emoji image in the server storage system. Will be something like: +	// '/gotosocial/storage/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/original/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' +	ImagePath string `pg:",notnull"` +	// Path of a static version of the emoji image in the server storage system. Will be something like: +	// '/gotosocial/storage/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/small/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' +	ImageStaticPath string `pg:",notnull"` +	// MIME content type of the emoji image +	// Probably "image/png" +	ImageContentType string `pg:",notnull"` +	// Size of the emoji image file in bytes, for serving purposes. +	ImageFileSize int `pg:",notnull"` +	// Size of the static version of the emoji image file in bytes, for serving purposes. +	ImageStaticFileSize int `pg:",notnull"` +	// When was the emoji image last updated? +	ImageUpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` +	// Has a moderation action disabled this emoji from being shown? +	Disabled bool `pg:",notnull,default:false"` +	// ActivityStreams uri of this emoji. Something like 'https://example.org/emojis/1234' +	URI string `pg:",notnull,unique"` +	// Is this emoji visible in the admin emoji picker? +	VisibleInPicker bool `pg:",notnull,default:true"` +	// In which emoji category is this emoji visible? +	CategoryID string +} diff --git a/internal/db/model/follow.go b/internal/db/gtsmodel/follow.go index 36e19e72e..90080da6e 100644 --- a/internal/db/model/follow.go +++ b/internal/db/gtsmodel/follow.go @@ -16,7 +16,7 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package model +package gtsmodel  import "time" diff --git a/internal/db/model/followrequest.go b/internal/db/gtsmodel/followrequest.go index 50d8a5f03..1401a26f1 100644 --- a/internal/db/model/followrequest.go +++ b/internal/db/gtsmodel/followrequest.go @@ -16,7 +16,7 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package model +package gtsmodel  import "time" diff --git a/internal/db/model/mediaattachment.go b/internal/db/gtsmodel/mediaattachment.go index 3aff18d80..d2b028b18 100644 --- a/internal/db/model/mediaattachment.go +++ b/internal/db/gtsmodel/mediaattachment.go @@ -16,7 +16,7 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package model +package gtsmodel  import (  	"time" @@ -29,7 +29,9 @@ type MediaAttachment struct {  	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`  	// ID of the status to which this is attached  	StatusID string -	// Where can the attachment be retrieved on a remote server +	// Where can the attachment be retrieved on *this* server +	URL string +	// Where can the attachment be retrieved on a remote server (empty for local media)  	RemoteURL string  	// When was the attachment created  	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` @@ -81,7 +83,9 @@ type Thumbnail struct {  	FileSize int  	// When was the file last updated  	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` -	// What is the remote URL of the thumbnail +	// What is the URL of the thumbnail on the local server +	URL string +	// What is the remote URL of the thumbnail (empty for local media)  	RemoteURL string  } @@ -111,15 +115,18 @@ const (  	FileTypeAudio FileType = "audio"  	// FileTypeVideo is for files with audio + visual  	FileTypeVideo FileType = "video" +	// FileTypeUnknown is for unknown file types (surprise surprise!) +	FileTypeUnknown FileType = "unknown"  )  // FileMeta describes metadata about the actual contents of the file.  type FileMeta struct {  	Original Original  	Small    Small +	Focus    Focus  } -// Small implements SmallMeta and can be used for a thumbnail of any media type +// Small can be used for a thumbnail of any media type  type Small struct {  	Width  int  	Height int @@ -127,10 +134,15 @@ type Small struct {  	Aspect float64  } -// ImageOriginal implements OriginalMeta for still images +// Original can be used for original metadata for any media type  type Original struct {  	Width  int  	Height int  	Size   int  	Aspect float64  } + +type Focus struct { +	X float32 +	Y float32 +} diff --git a/internal/db/gtsmodel/mention.go b/internal/db/gtsmodel/mention.go new file mode 100644 index 000000000..18eb11082 --- /dev/null +++ b/internal/db/gtsmodel/mention.go @@ -0,0 +1,39 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel + +import "time" + +// Mention refers to the 'tagging' or 'mention' of a user within a status. +type Mention struct { +	// ID of this mention in the database +	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` +	// ID of the status this mention originates from +	StatusID string `pg:",notnull"` +	// 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()"` +	// Who created this mention? +	OriginAccountID string `pg:",notnull"` +	// Who does this mention target? +	TargetAccountID string `pg:",notnull"` +	// Prevent this mention from generating a notification? +	Silent bool +} diff --git a/internal/db/gtsmodel/poll.go b/internal/db/gtsmodel/poll.go new file mode 100644 index 000000000..c39497cdd --- /dev/null +++ b/internal/db/gtsmodel/poll.go @@ -0,0 +1,19 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel diff --git a/internal/db/gtsmodel/status.go b/internal/db/gtsmodel/status.go new file mode 100644 index 000000000..3b4b84405 --- /dev/null +++ b/internal/db/gtsmodel/status.go @@ -0,0 +1,138 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel + +import "time" + +// Status represents a user-created 'post' or 'status' in the database, either remote or local +type Status struct { +	// id of the status in the database +	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` +	// uri at which this status is reachable +	URI string `pg:",unique"` +	// web url for viewing this status +	URL string `pg:",unique"` +	// the html-formatted content of this status +	Content string +	// Database IDs of any media attachments associated with this status +	Attachments []string `pg:",array"` +	// Database IDs of any tags used in this status +	Tags []string `pg:",array"` +	// Database IDs of any accounts mentioned in this status +	Mentions []string `pg:",array"` +	// Database IDs of any emojis used in this status +	Emojis []string `pg:",array"` +	// when was this status created? +	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` +	// when was this status updated? +	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` +	// is this status from a local account? +	Local bool +	// which account posted this status? +	AccountID string +	// id of the status this status is a reply to +	InReplyToID string +	// id of the account that this status replies to +	InReplyToAccountID string +	// id of the status this status is a boost of +	BoostOfID string +	// cw string for this status +	ContentWarning string +	// visibility entry for this status +	Visibility Visibility `pg:",notnull"` +	// mark the status as sensitive? +	Sensitive bool +	// what language is this status written in? +	Language string +	// Which application was used to create this status? +	CreatedWithApplicationID string +	// advanced visibility for this status +	VisibilityAdvanced *VisibilityAdvanced +	// What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types +	// Will probably almost always be Note but who knows!. +	ActivityStreamsType ActivityStreamsObject +	// Original text of the status without formatting +	Text string + +	/* +		NON-DATABASE FIELDS + +		These are for convenience while passing the status around internally, +		but these fields should *never* be put in the db. +	*/ + +	// Mentions created in this status +	GTSMentions []*Mention `pg:"-"` +	// Hashtags used in this status +	GTSTags []*Tag `pg:"-"` +	// Emojis used in this status +	GTSEmojis []*Emoji `pg:"-"` +	// MediaAttachments used in this status +	GTSMediaAttachments []*MediaAttachment `pg:"-"` +	// Status being replied to +	GTSReplyToStatus *Status `pg:"-"` +	// Account being replied to +	GTSReplyToAccount *Account `pg:"-"` +} + +// Visibility represents the visibility granularity of a status. +type Visibility string + +const ( +	// This status will be visible to everyone on all timelines. +	VisibilityPublic Visibility = "public" +	// This status will be visible to everyone, but will only show on home timeline to followers, and in lists. +	VisibilityUnlocked Visibility = "unlocked" +	// This status is viewable to followers only. +	VisibilityFollowersOnly Visibility = "followers_only" +	// This status is visible to mutual followers only. +	VisibilityMutualsOnly Visibility = "mutuals_only" +	// This status is visible only to mentioned recipients +	VisibilityDirect Visibility = "direct" +	// Default visibility to use when no other setting can be found +	VisibilityDefault Visibility = "public" +) + +// VisibilityAdvanced denotes a set of flags that can be set on a status for fine-tuning visibility and interactivity of the status. +type VisibilityAdvanced struct { +	/* +		ADVANCED SETTINGS -- These should all default to TRUE. + +		If PUBLIC is selected, they will all be overwritten to TRUE regardless of what is selected. +		If UNLOCKED is selected, any of them can be turned on or off in any combination. +		If FOLLOWERS-ONLY or MUTUALS-ONLY are selected, boostable will always be FALSE. The others can be turned on or off as desired. +		If DIRECT is selected, boostable will be FALSE, and all other flags will be TRUE. +	*/ +	// This status will be federated beyond the local timeline(s) +	Federated bool `pg:"default:true"` +	// This status can be boosted/reblogged +	Boostable bool `pg:"default:true"` +	// This status can be replied to +	Replyable bool `pg:"default:true"` +	// This status can be liked/faved +	Likeable bool `pg:"default:true"` +} + +// RelevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status. +type RelevantAccounts struct { +	ReplyToAccount        *Account +	BoostedAccount        *Account +	BoostedReplyToAccount *Account +	MentionedAccounts     []*Account +} diff --git a/internal/db/gtsmodel/statusbookmark.go b/internal/db/gtsmodel/statusbookmark.go new file mode 100644 index 000000000..6246334e3 --- /dev/null +++ b/internal/db/gtsmodel/statusbookmark.go @@ -0,0 +1,35 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel + +import "time" + +// StatusBookmark refers to one account having a 'bookmark' of the status of another account +type StatusBookmark struct { +	// id of this bookmark in the database +	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` +	// 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:",notnull"` +	// id the account owning the bookmarked status +	TargetAccountID string `pg:",notnull"` +	// database id of the status that has been bookmarked +	StatusID string `pg:",notnull"` +} diff --git a/internal/db/gtsmodel/statusfave.go b/internal/db/gtsmodel/statusfave.go new file mode 100644 index 000000000..9fb92b931 --- /dev/null +++ b/internal/db/gtsmodel/statusfave.go @@ -0,0 +1,38 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel + +import "time" + +// StatusFave refers to a 'fave' or 'like' in the database, from one account, targeting the status of another account +type StatusFave struct { +	// id of this fave in the database +	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` +	// 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:",notnull"` +	// id the account owning the faved status +	TargetAccountID string `pg:",notnull"` +	// database id of the status that has been 'faved' +	StatusID string `pg:",notnull"` + +	// FavedStatus is the status being interacted with. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. +	FavedStatus *Status `pg:"-"` +} diff --git a/internal/db/gtsmodel/statusmute.go b/internal/db/gtsmodel/statusmute.go new file mode 100644 index 000000000..53c15e5b5 --- /dev/null +++ b/internal/db/gtsmodel/statusmute.go @@ -0,0 +1,35 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel + +import "time" + +// StatusMute refers to one account having muted the status of another account or its own +type StatusMute struct { +	// id of this mute in the database +	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` +	// 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:",notnull"` +	// id the account owning the muted status (can be the same as accountID) +	TargetAccountID string `pg:",notnull"` +	// database id of the status that has been muted +	StatusID string `pg:",notnull"` +} diff --git a/internal/db/gtsmodel/statuspin.go b/internal/db/gtsmodel/statuspin.go new file mode 100644 index 000000000..1df333387 --- /dev/null +++ b/internal/db/gtsmodel/statuspin.go @@ -0,0 +1,33 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel + +import "time" + +// StatusPin refers to a status 'pinned' to the top of an account +type StatusPin struct { +	// id of this pin in the database +	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` +	// when was this pin created +	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` +	// id of the account that created ('did') the pinning (this should always be the same as the author of the status) +	AccountID string `pg:",notnull"` +	// database id of the status that has been pinned +	StatusID string `pg:",notnull"` +} diff --git a/internal/db/gtsmodel/tag.go b/internal/db/gtsmodel/tag.go new file mode 100644 index 000000000..83c471958 --- /dev/null +++ b/internal/db/gtsmodel/tag.go @@ -0,0 +1,41 @@ +/* +   GoToSocial +   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + +   This program is free software: you can redistribute it and/or modify +   it under the terms of the GNU Affero General Public License as published by +   the Free Software Foundation, either version 3 of the License, or +   (at your option) any later version. + +   This program is distributed in the hope that it will be useful, +   but WITHOUT ANY WARRANTY; without even the implied warranty of +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +   GNU Affero General Public License for more details. + +   You should have received a copy of the GNU Affero General Public License +   along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +package gtsmodel + +import "time" + +// Tag represents a hashtag for gathering public statuses together +type Tag struct { +	// id of this tag in the database +	ID string `pg:",unique,type:uuid,default:gen_random_uuid(),pk,notnull"` +	// name of this tag -- the tag without the hash part +	Name string `pg:",unique,pk,notnull"` +	// Which account ID is the first one we saw using this tag? +	FirstSeenFromAccountID string +	// when was this tag created +	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` +	// when was this tag last updated +	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` +	// can our instance users use this tag? +	Useable bool `pg:",notnull,default:true"` +	// can our instance users look up this tag? +	Listable bool `pg:",notnull,default:true"` +	// when was this tag last used? +	LastStatusAt time.Time `pg:"type:timestamp,notnull,default:now()"` +} diff --git a/internal/db/model/user.go b/internal/db/gtsmodel/user.go index 61e9954d5..a72569945 100644 --- a/internal/db/model/user.go +++ b/internal/db/gtsmodel/user.go @@ -16,7 +16,7 @@     along with this program.  If not, see <http://www.gnu.org/licenses/>.  */ -package model +package gtsmodel  import (  	"net" diff --git a/internal/db/mock_DB.go b/internal/db/mock_DB.go index d4c25bb79..df2e41907 100644 --- a/internal/db/mock_DB.go +++ b/internal/db/mock_DB.go @@ -6,9 +6,7 @@ import (  	context "context"  	mock "github.com/stretchr/testify/mock" -	mastotypes "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" - -	model "github.com/superseriousbusiness/gotosocial/internal/db/model" +	gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"  	net "net" @@ -20,22 +18,20 @@ type MockDB struct {  	mock.Mock  } -// AccountToMastoSensitive provides a mock function with given fields: account -func (_m *MockDB) AccountToMastoSensitive(account *model.Account) (*mastotypes.Account, error) { -	ret := _m.Called(account) +// Blocked provides a mock function with given fields: account1, account2 +func (_m *MockDB) Blocked(account1 string, account2 string) (bool, error) { +	ret := _m.Called(account1, account2) -	var r0 *mastotypes.Account -	if rf, ok := ret.Get(0).(func(*model.Account) *mastotypes.Account); ok { -		r0 = rf(account) +	var r0 bool +	if rf, ok := ret.Get(0).(func(string, string) bool); ok { +		r0 = rf(account1, account2)  	} else { -		if ret.Get(0) != nil { -			r0 = ret.Get(0).(*mastotypes.Account) -		} +		r0 = ret.Get(0).(bool)  	}  	var r1 error -	if rf, ok := ret.Get(1).(func(*model.Account) error); ok { -		r1 = rf(account) +	if rf, ok := ret.Get(1).(func(string, string) error); ok { +		r1 = rf(account1, account2)  	} else {  		r1 = ret.Error(1)  	} @@ -99,6 +95,29 @@ func (_m *MockDB) DropTable(i interface{}) error {  	return r0  } +// EmojiStringsToEmojis provides a mock function with given fields: emojis, originAccountID, statusID +func (_m *MockDB) EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error) { +	ret := _m.Called(emojis, originAccountID, statusID) + +	var r0 []*gtsmodel.Emoji +	if rf, ok := ret.Get(0).(func([]string, string, string) []*gtsmodel.Emoji); ok { +		r0 = rf(emojis, originAccountID, statusID) +	} else { +		if ret.Get(0) != nil { +			r0 = ret.Get(0).([]*gtsmodel.Emoji) +		} +	} + +	var r1 error +	if rf, ok := ret.Get(1).(func([]string, string, string) error); ok { +		r1 = rf(emojis, originAccountID, statusID) +	} else { +		r1 = ret.Error(1) +	} + +	return r0, r1 +} +  // Federation provides a mock function with given fields:  func (_m *MockDB) Federation() pub.Database {  	ret := _m.Called() @@ -116,11 +135,11 @@ func (_m *MockDB) Federation() pub.Database {  }  // GetAccountByUserID provides a mock function with given fields: userID, account -func (_m *MockDB) GetAccountByUserID(userID string, account *model.Account) error { +func (_m *MockDB) GetAccountByUserID(userID string, account *gtsmodel.Account) error {  	ret := _m.Called(userID, account)  	var r0 error -	if rf, ok := ret.Get(0).(func(string, *model.Account) error); ok { +	if rf, ok := ret.Get(0).(func(string, *gtsmodel.Account) error); ok {  		r0 = rf(userID, account)  	} else {  		r0 = ret.Error(0) @@ -143,6 +162,20 @@ func (_m *MockDB) GetAll(i interface{}) error {  	return r0  } +// GetAvatarForAccountID provides a mock function with given fields: avatar, accountID +func (_m *MockDB) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error { +	ret := _m.Called(avatar, accountID) + +	var r0 error +	if rf, ok := ret.Get(0).(func(*gtsmodel.MediaAttachment, string) error); ok { +		r0 = rf(avatar, accountID) +	} else { +		r0 = ret.Error(0) +	} + +	return r0 +} +  // GetByID provides a mock function with given fields: id, i  func (_m *MockDB) GetByID(id string, i interface{}) error {  	ret := _m.Called(id, i) @@ -158,11 +191,11 @@ func (_m *MockDB) GetByID(id string, i interface{}) error {  }  // GetFollowRequestsForAccountID provides a mock function with given fields: accountID, followRequests -func (_m *MockDB) GetFollowRequestsForAccountID(accountID string, followRequests *[]model.FollowRequest) error { +func (_m *MockDB) GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error {  	ret := _m.Called(accountID, followRequests)  	var r0 error -	if rf, ok := ret.Get(0).(func(string, *[]model.FollowRequest) error); ok { +	if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.FollowRequest) error); ok {  		r0 = rf(accountID, followRequests)  	} else {  		r0 = ret.Error(0) @@ -172,11 +205,11 @@ func (_m *MockDB) GetFollowRequestsForAccountID(accountID string, followRequests  }  // GetFollowersByAccountID provides a mock function with given fields: accountID, followers -func (_m *MockDB) GetFollowersByAccountID(accountID string, followers *[]model.Follow) error { +func (_m *MockDB) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error {  	ret := _m.Called(accountID, followers)  	var r0 error -	if rf, ok := ret.Get(0).(func(string, *[]model.Follow) error); ok { +	if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Follow) error); ok {  		r0 = rf(accountID, followers)  	} else {  		r0 = ret.Error(0) @@ -186,11 +219,11 @@ func (_m *MockDB) GetFollowersByAccountID(accountID string, followers *[]model.F  }  // GetFollowingByAccountID provides a mock function with given fields: accountID, following -func (_m *MockDB) GetFollowingByAccountID(accountID string, following *[]model.Follow) error { +func (_m *MockDB) GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error {  	ret := _m.Called(accountID, following)  	var r0 error -	if rf, ok := ret.Get(0).(func(string, *[]model.Follow) error); ok { +	if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Follow) error); ok {  		r0 = rf(accountID, following)  	} else {  		r0 = ret.Error(0) @@ -199,12 +232,26 @@ func (_m *MockDB) GetFollowingByAccountID(accountID string, following *[]model.F  	return r0  } +// GetHeaderForAccountID provides a mock function with given fields: header, accountID +func (_m *MockDB) GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error { +	ret := _m.Called(header, accountID) + +	var r0 error +	if rf, ok := ret.Get(0).(func(*gtsmodel.MediaAttachment, string) error); ok { +		r0 = rf(header, accountID) +	} else { +		r0 = ret.Error(0) +	} + +	return r0 +} +  // GetLastStatusForAccountID provides a mock function with given fields: accountID, status -func (_m *MockDB) GetLastStatusForAccountID(accountID string, status *model.Status) error { +func (_m *MockDB) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error {  	ret := _m.Called(accountID, status)  	var r0 error -	if rf, ok := ret.Get(0).(func(string, *model.Status) error); ok { +	if rf, ok := ret.Get(0).(func(string, *gtsmodel.Status) error); ok {  		r0 = rf(accountID, status)  	} else {  		r0 = ret.Error(0) @@ -214,11 +261,11 @@ func (_m *MockDB) GetLastStatusForAccountID(accountID string, status *model.Stat  }  // GetStatusesByAccountID provides a mock function with given fields: accountID, statuses -func (_m *MockDB) GetStatusesByAccountID(accountID string, statuses *[]model.Status) error { +func (_m *MockDB) GetStatusesByAccountID(accountID string, statuses *[]gtsmodel.Status) error {  	ret := _m.Called(accountID, statuses)  	var r0 error -	if rf, ok := ret.Get(0).(func(string, *[]model.Status) error); ok { +	if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Status) error); ok {  		r0 = rf(accountID, statuses)  	} else {  		r0 = ret.Error(0) @@ -228,11 +275,11 @@ func (_m *MockDB) GetStatusesByAccountID(accountID string, statuses *[]model.Sta  }  // GetStatusesByTimeDescending provides a mock function with given fields: accountID, statuses, limit -func (_m *MockDB) GetStatusesByTimeDescending(accountID string, statuses *[]model.Status, limit int) error { +func (_m *MockDB) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int) error {  	ret := _m.Called(accountID, statuses, limit)  	var r0 error -	if rf, ok := ret.Get(0).(func(string, *[]model.Status, int) error); ok { +	if rf, ok := ret.Get(0).(func(string, *[]gtsmodel.Status, int) error); ok {  		r0 = rf(accountID, statuses, limit)  	} else {  		r0 = ret.Error(0) @@ -297,16 +344,39 @@ func (_m *MockDB) IsUsernameAvailable(username string) error {  	return r0  } +// MentionStringsToMentions provides a mock function with given fields: targetAccounts, originAccountID, statusID +func (_m *MockDB) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) { +	ret := _m.Called(targetAccounts, originAccountID, statusID) + +	var r0 []*gtsmodel.Mention +	if rf, ok := ret.Get(0).(func([]string, string, string) []*gtsmodel.Mention); ok { +		r0 = rf(targetAccounts, originAccountID, statusID) +	} else { +		if ret.Get(0) != nil { +			r0 = ret.Get(0).([]*gtsmodel.Mention) +		} +	} + +	var r1 error +	if rf, ok := ret.Get(1).(func([]string, string, string) error); ok { +		r1 = rf(targetAccounts, originAccountID, statusID) +	} else { +		r1 = ret.Error(1) +	} + +	return r0, r1 +} +  // NewSignup provides a mock function with given fields: username, reason, requireApproval, email, password, signUpIP, locale, appID -func (_m *MockDB) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*model.User, error) { +func (_m *MockDB) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*gtsmodel.User, error) {  	ret := _m.Called(username, reason, requireApproval, email, password, signUpIP, locale, appID) -	var r0 *model.User -	if rf, ok := ret.Get(0).(func(string, string, bool, string, string, net.IP, string, string) *model.User); ok { +	var r0 *gtsmodel.User +	if rf, ok := ret.Get(0).(func(string, string, bool, string, string, net.IP, string, string) *gtsmodel.User); ok {  		r0 = rf(username, reason, requireApproval, email, password, signUpIP, locale, appID)  	} else {  		if ret.Get(0) != nil { -			r0 = ret.Get(0).(*model.User) +			r0 = ret.Get(0).(*gtsmodel.User)  		}  	} @@ -334,6 +404,20 @@ func (_m *MockDB) Put(i interface{}) error {  	return r0  } +// SetHeaderOrAvatarForAccountID provides a mock function with given fields: mediaAttachment, accountID +func (_m *MockDB) SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error { +	ret := _m.Called(mediaAttachment, accountID) + +	var r0 error +	if rf, ok := ret.Get(0).(func(*gtsmodel.MediaAttachment, string) error); ok { +		r0 = rf(mediaAttachment, accountID) +	} else { +		r0 = ret.Error(0) +	} + +	return r0 +} +  // Stop provides a mock function with given fields: ctx  func (_m *MockDB) Stop(ctx context.Context) error {  	ret := _m.Called(ctx) @@ -348,6 +432,29 @@ func (_m *MockDB) Stop(ctx context.Context) error {  	return r0  } +// TagStringsToTags provides a mock function with given fields: tags, originAccountID, statusID +func (_m *MockDB) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) { +	ret := _m.Called(tags, originAccountID, statusID) + +	var r0 []*gtsmodel.Tag +	if rf, ok := ret.Get(0).(func([]string, string, string) []*gtsmodel.Tag); ok { +		r0 = rf(tags, originAccountID, statusID) +	} else { +		if ret.Get(0) != nil { +			r0 = ret.Get(0).([]*gtsmodel.Tag) +		} +	} + +	var r1 error +	if rf, ok := ret.Get(1).(func([]string, string, string) error); ok { +		r1 = rf(tags, originAccountID, statusID) +	} else { +		r1 = ret.Error(1) +	} + +	return r0, r1 +} +  // UpdateByID provides a mock function with given fields: id, i  func (_m *MockDB) UpdateByID(id string, i interface{}) error {  	ret := _m.Called(id, i) @@ -361,3 +468,17 @@ func (_m *MockDB) UpdateByID(id string, i interface{}) error {  	return r0  } + +// UpdateOneByID provides a mock function with given fields: id, key, value, i +func (_m *MockDB) UpdateOneByID(id string, key string, value interface{}, i interface{}) error { +	ret := _m.Called(id, key, value, i) + +	var r0 error +	if rf, ok := ret.Get(0).(func(string, string, interface{}, interface{}) error); ok { +		r0 = rf(id, key, value, i) +	} else { +		r0 = ret.Error(0) +	} + +	return r0 +} diff --git a/internal/db/model/status.go b/internal/db/model/status.go deleted file mode 100644 index d15258727..000000000 --- a/internal/db/model/status.go +++ /dev/null @@ -1,63 +0,0 @@ -/* -   GoToSocial -   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org - -   This program is free software: you can redistribute it and/or modify -   it under the terms of the GNU Affero General Public License as published by -   the Free Software Foundation, either version 3 of the License, or -   (at your option) any later version. - -   This program is distributed in the hope that it will be useful, -   but WITHOUT ANY WARRANTY; without even the implied warranty of -   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -   GNU Affero General Public License for more details. - -   You should have received a copy of the GNU Affero General Public License -   along with this program.  If not, see <http://www.gnu.org/licenses/>. -*/ - -package model - -import "time" - -// Status represents a user-created 'post' or 'status' in the database, either remote or local -type Status struct { -	// id of the status in the database -	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` -	// uri at which this status is reachable -	URI string `pg:",unique"` -	// web url for viewing this status -	URL string `pg:",unique"` -	// the html-formatted content of this status -	Content string -	// when was this status created? -	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` -	// when was this status updated? -	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` -	// is this status from a local account? -	Local bool -	// which account posted this status? -	AccountID string -	// id of the status this status is a reply to -	InReplyToID string -	// id of the status this status is a boost of -	BoostOfID string -	// cw string for this status -	ContentWarning string -	// visibility entry for this status -	Visibility *Visibility -} - -// Visibility represents the visibility granularity of a status. It is a combination of flags. -type Visibility struct { -	// Is this status viewable as a direct message? -	Direct bool -	// Is this status viewable to followers? -	Followers bool -	// Is this status viewable on the local timeline? -	Local bool -	// Is this status boostable but not shown on public timelines? -	Unlisted bool -	// Is this status shown on public and federated timelines? -	Public bool -} diff --git a/internal/db/pg.go b/internal/db/pg.go index df01132c2..a12529d00 100644 --- a/internal/db/pg.go +++ b/internal/db/pg.go @@ -34,11 +34,11 @@ import (  	"github.com/go-pg/pg/extra/pgdebug"  	"github.com/go-pg/pg/v10"  	"github.com/go-pg/pg/v10/orm" +	"github.com/google/uuid"  	"github.com/sirupsen/logrus"  	"github.com/superseriousbusiness/gotosocial/internal/config" -	"github.com/superseriousbusiness/gotosocial/internal/db/model" +	"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/util" -	"github.com/superseriousbusiness/gotosocial/pkg/mastotypes"  	"golang.org/x/crypto/bcrypt"  ) @@ -60,12 +60,6 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry  	}  	log.Debugf("using pg options: %+v", opts) -	readyChan := make(chan interface{}) -	opts.OnConnect = func(ctx context.Context, c *pg.Conn) error { -		close(readyChan) -		return nil -	} -  	// create a connection  	pgCtx, cancel := context.WithCancel(ctx)  	conn := pg.Connect(opts).WithContext(pgCtx) @@ -80,8 +74,7 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry  		})  	} -	// actually *begin* the connection so that we can tell if the db is there -	// and listening, and also trigger the opts.OnConnect function passed in above +	// actually *begin* the connection so that we can tell if the db is there and listening  	if err := conn.Ping(ctx); err != nil {  		cancel()  		return nil, fmt.Errorf("db connection error: %s", err) @@ -95,16 +88,6 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry  	}  	log.Infof("connected to postgres version: %s", version) -	// make sure the opts.OnConnect function has been triggered -	// and closed the ready channel -	select { -	case <-readyChan: -		log.Infof("postgres connection ready") -	case <-time.After(5 * time.Second): -		cancel() -		return nil, errors.New("db connection timeout") -	} -  	ps := &postgresService{  		config: c,  		conn:   conn, @@ -214,9 +197,9 @@ func (ps *postgresService) IsHealthy(ctx context.Context) error {  func (ps *postgresService) CreateSchema(ctx context.Context) error {  	models := []interface{}{ -		(*model.Account)(nil), -		(*model.Status)(nil), -		(*model.User)(nil), +		(*gtsmodel.Account)(nil), +		(*gtsmodel.Status)(nil), +		(*gtsmodel.User)(nil),  	}  	ps.log.Info("creating db schema") @@ -254,6 +237,10 @@ func (ps *postgresService) GetWhere(key string, value interface{}, i interface{}  	return nil  } +// func (ps *postgresService) GetWhereMany(i interface{}, where ...model.Where) error { +// 	return nil +// } +  func (ps *postgresService) GetAll(i interface{}) error {  	if err := ps.conn.Model(i).Select(); err != nil {  		if err == pg.ErrNoRows { @@ -269,8 +256,18 @@ func (ps *postgresService) Put(i interface{}) error {  	return err  } +func (ps *postgresService) Upsert(i interface{}, conflictColumn string) error { +	if _, err := ps.conn.Model(i).OnConflict(fmt.Sprintf("(%s) DO UPDATE", conflictColumn)).Insert(); err != nil { +		if err == pg.ErrNoRows { +			return ErrNoEntries{} +		} +		return err +	} +	return nil +} +  func (ps *postgresService) UpdateByID(id string, i interface{}) error { -	if _, err := ps.conn.Model(i).OnConflict("(id) DO UPDATE").Insert(); err != nil { +	if _, err := ps.conn.Model(i).Where("id = ?", id).OnConflict("(id) DO UPDATE").Insert(); err != nil {  		if err == pg.ErrNoRows {  			return ErrNoEntries{}  		} @@ -308,8 +305,25 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac  	HANDY SHORTCUTS  */ -func (ps *postgresService) GetAccountByUserID(userID string, account *model.Account) error { -	user := &model.User{ +func (ps *postgresService) CreateInstanceAccount() error { +	username := ps.config.Host +	instanceAccount := >smodel.Account{ +		Username: username, +	} +	inserted, err := ps.conn.Model(instanceAccount).Where("username = ?", username).SelectOrInsert() +	if err != nil { +		return err +	} +	if inserted { +		ps.log.Infof("created instance account %s with id %s", username, instanceAccount.ID) +	} else { +		ps.log.Infof("instance account %s already exists with id %s", username, instanceAccount.ID) +	} +	return nil +} + +func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.Account) error { +	user := >smodel.User{  		ID: userID,  	}  	if err := ps.conn.Model(user).Where("id = ?", userID).Select(); err != nil { @@ -327,7 +341,7 @@ func (ps *postgresService) GetAccountByUserID(userID string, account *model.Acco  	return nil  } -func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, followRequests *[]model.FollowRequest) error { +func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error {  	if err := ps.conn.Model(followRequests).Where("target_account_id = ?", accountID).Select(); err != nil {  		if err == pg.ErrNoRows {  			return ErrNoEntries{} @@ -337,7 +351,7 @@ func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, follo  	return nil  } -func (ps *postgresService) GetFollowingByAccountID(accountID string, following *[]model.Follow) error { +func (ps *postgresService) GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error {  	if err := ps.conn.Model(following).Where("account_id = ?", accountID).Select(); err != nil {  		if err == pg.ErrNoRows {  			return ErrNoEntries{} @@ -347,7 +361,7 @@ func (ps *postgresService) GetFollowingByAccountID(accountID string, following *  	return nil  } -func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]model.Follow) error { +func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error {  	if err := ps.conn.Model(followers).Where("target_account_id = ?", accountID).Select(); err != nil {  		if err == pg.ErrNoRows {  			return ErrNoEntries{} @@ -357,7 +371,7 @@ func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *  	return nil  } -func (ps *postgresService) GetStatusesByAccountID(accountID string, statuses *[]model.Status) error { +func (ps *postgresService) GetStatusesByAccountID(accountID string, statuses *[]gtsmodel.Status) error {  	if err := ps.conn.Model(statuses).Where("account_id = ?", accountID).Select(); err != nil {  		if err == pg.ErrNoRows {  			return ErrNoEntries{} @@ -367,7 +381,7 @@ func (ps *postgresService) GetStatusesByAccountID(accountID string, statuses *[]  	return nil  } -func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]model.Status, limit int) error { +func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int) error {  	q := ps.conn.Model(statuses).Order("created_at DESC")  	if limit != 0 {  		q = q.Limit(limit) @@ -384,7 +398,7 @@ func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuse  	return nil  } -func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *model.Status) error { +func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error {  	if err := ps.conn.Model(status).Order("created_at DESC").Limit(1).Where("account_id = ?", accountID).Select(); err != nil {  		if err == pg.ErrNoRows {  			return ErrNoEntries{} @@ -399,7 +413,7 @@ func (ps *postgresService) IsUsernameAvailable(username string) error {  	// if no error we fail because it means we found something  	// if error but it's not pg.ErrNoRows then we fail  	// if err is pg.ErrNoRows we're good, we found nothing so continue -	if err := ps.conn.Model(&model.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil { +	if err := ps.conn.Model(>smodel.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil {  		return fmt.Errorf("username %s already in use", username)  	} else if err != pg.ErrNoRows {  		return fmt.Errorf("db error: %s", err) @@ -416,7 +430,7 @@ func (ps *postgresService) IsEmailAvailable(email string) error {  	domain := strings.Split(m.Address, "@")[1] // domain will always be the second part after @  	// check if the email domain is blocked -	if err := ps.conn.Model(&model.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil { +	if err := ps.conn.Model(>smodel.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil {  		// fail because we found something  		return fmt.Errorf("email domain %s is blocked", domain)  	} else if err != pg.ErrNoRows { @@ -425,7 +439,7 @@ func (ps *postgresService) IsEmailAvailable(email string) error {  	}  	// check if this email is associated with a user already -	if err := ps.conn.Model(&model.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil { +	if err := ps.conn.Model(>smodel.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil {  		// fail because we found something  		return fmt.Errorf("email %s already in use", email)  	} else if err != pg.ErrNoRows { @@ -435,7 +449,7 @@ func (ps *postgresService) IsEmailAvailable(email string) error {  	return nil  } -func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*model.User, error) { +func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*gtsmodel.User, error) {  	key, err := rsa.GenerateKey(rand.Reader, 2048)  	if err != nil {  		ps.log.Errorf("error creating new rsa key: %s", err) @@ -444,19 +458,19 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr  	uris := util.GenerateURIs(username, ps.config.Protocol, ps.config.Host) -	a := &model.Account{ +	a := >smodel.Account{  		Username:              username,  		DisplayName:           username,  		Reason:                reason,  		URL:                   uris.UserURL,  		PrivateKey:            key,  		PublicKey:             &key.PublicKey, -		ActorType:             "Person", +		ActorType:             gtsmodel.ActivityStreamsPerson,  		URI:                   uris.UserURI, -		InboxURL:              uris.InboxURL, -		OutboxURL:             uris.OutboxURL, -		FollowersURL:          uris.FollowersURL, -		FeaturedCollectionURL: uris.CollectionURL, +		InboxURL:              uris.InboxURI, +		OutboxURL:             uris.OutboxURI, +		FollowersURL:          uris.FollowersURI, +		FeaturedCollectionURL: uris.CollectionURI,  	}  	if _, err = ps.conn.Model(a).Insert(); err != nil {  		return nil, err @@ -466,7 +480,7 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr  	if err != nil {  		return nil, fmt.Errorf("error hashing password: %s", err)  	} -	u := &model.User{ +	u := >smodel.User{  		AccountID:              a.ID,  		EncryptedPassword:      string(pw),  		SignUpIP:               signUpIP, @@ -482,13 +496,45 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr  	return u, nil  } -func (ps *postgresService) SetHeaderOrAvatarForAccountID(mediaAttachment *model.MediaAttachment, accountID string) error { -	_, err := ps.conn.Model(mediaAttachment).Insert() -	return err +func (ps *postgresService) SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error { +	if mediaAttachment.Avatar && mediaAttachment.Header { +		return errors.New("one media attachment cannot be both header and avatar") +	} + +	var headerOrAVI string +	if mediaAttachment.Avatar { +		headerOrAVI = "avatar" +	} else if mediaAttachment.Header { +		headerOrAVI = "header" +	} else { +		return errors.New("given media attachment was neither a header nor an avatar") +	} + +	// TODO: there are probably more side effects here that need to be handled +	if _, err := ps.conn.Model(mediaAttachment).OnConflict("(id) DO UPDATE").Insert(); err != nil { +		return err +	} + +	if _, err := ps.conn.Model(>smodel.Account{}).Set(fmt.Sprintf("%s_media_attachment_id = ?", headerOrAVI), mediaAttachment.ID).Where("id = ?", accountID).Update(); err != nil { +		return err +	} +	return nil  } -func (ps *postgresService) GetHeaderForAccountID(header *model.MediaAttachment, accountID string) error { -	if err := ps.conn.Model(header).Where("account_id = ?", accountID).Where("header = ?", true).Select(); err != nil { +func (ps *postgresService) GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error { +	acct := >smodel.Account{} +	if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { +		if err == pg.ErrNoRows { +			return ErrNoEntries{} +		} +		return err +	} + +	if acct.HeaderMediaAttachmentID == "" { +		return ErrNoEntries{} +	} + +	if err := ps.conn.Model(header).Where("id = ?", acct.HeaderMediaAttachmentID).Select(); err != nil {  		if err == pg.ErrNoRows {  			return ErrNoEntries{}  		} @@ -497,8 +543,20 @@ func (ps *postgresService) GetHeaderForAccountID(header *model.MediaAttachment,  	return nil  } -func (ps *postgresService) GetAvatarForAccountID(avatar *model.MediaAttachment, accountID string) error { -	if err := ps.conn.Model(avatar).Where("account_id = ?", accountID).Where("avatar = ?", true).Select(); err != nil { +func (ps *postgresService) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error { +	acct := >smodel.Account{} +	if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { +		if err == pg.ErrNoRows { +			return ErrNoEntries{} +		} +		return err +	} + +	if acct.AvatarMediaAttachmentID == "" { +		return ErrNoEntries{} +	} + +	if err := ps.conn.Model(avatar).Where("id = ?", acct.AvatarMediaAttachmentID).Select(); err != nil {  		if err == pg.ErrNoRows {  			return ErrNoEntries{}  		} @@ -507,156 +565,480 @@ func (ps *postgresService) GetAvatarForAccountID(avatar *model.MediaAttachment,  	return nil  } -/* -	CONVERSION FUNCTIONS -*/ +func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) { +	var blocked bool +	if err := ps.conn.Model(>smodel.Block{}). +		Where("account_id = ?", account1).Where("target_account_id = ?", account2). +		WhereOr("target_account_id = ?", account1).Where("account_id = ?", account2). +		Select(); err != nil { +		if err == pg.ErrNoRows { +			blocked = false +			return blocked, nil +		} else { +			return blocked, err +		} +	} +	blocked = true +	return blocked, nil +} -// AccountToMastoSensitive takes an internal account model and transforms it into an account ready to be served through the API. -// The resulting account fits the specifications for the path /api/v1/accounts/verify_credentials, as described here: -// https://docs.joinmastodon.org/methods/accounts/. Note that it's *sensitive* because it's only meant to be exposed to the user -// that the account actually belongs to. -func (ps *postgresService) AccountToMastoSensitive(a *model.Account) (*mastotypes.Account, error) { -	// we can build this sensitive account easily by first getting the public account.... -	mastoAccount, err := ps.AccountToMastoPublic(a) -	if err != nil { -		return nil, err +func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) { +	l := ps.log.WithField("func", "StatusVisible") + +	// if target account is suspended then don't show the status +	if !targetAccount.SuspendedAt.IsZero() { +		l.Debug("target account suspended at is not zero") +		return false, nil  	} -	// then adding the Source object to it... +	// if the target user doesn't exist (anymore) then the status also shouldn't be visible +	targetUser := >smodel.User{} +	if err := ps.conn.Model(targetUser).Where("account_id = ?", targetAccount.ID).Select(); err != nil { +		l.Debug("target user could not be selected") +		if err == pg.ErrNoRows { +			return false, ErrNoEntries{} +		} else { +			return false, err +		} +	} -	// check pending follow requests aimed at this account -	fr := []model.FollowRequest{} -	if err := ps.GetFollowRequestsForAccountID(a.ID, &fr); err != nil { -		if _, ok := err.(ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting follow requests: %s", err) +	// if target user is disabled, not yet approved, or not confirmed then don't show the status +	// (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!) +	if targetUser.Disabled || !targetUser.Approved || targetUser.ConfirmedAt.IsZero() { +		l.Debug("target user is disabled, not approved, or not confirmed") +		return false, nil +	} + +	// If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed. +	// In this case, we can still serve the status if it's public, otherwise we definitely shouldn't. +	if requestingAccount == nil { + +		if targetStatus.Visibility == gtsmodel.VisibilityPublic { +			return true, nil  		} +		l.Debug("requesting account is nil but the target status isn't public") +		return false, nil  	} -	var frc int -	if fr != nil { -		frc = len(fr) + +	// if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten +	// this far (ie., been authed) in the first place: this is just for safety. +	if !requestingAccount.SuspendedAt.IsZero() { +		l.Debug("requesting account is suspended") +		return false, nil  	} -	mastoAccount.Source = &mastotypes.Source{ -		Privacy:             a.Privacy, -		Sensitive:           a.Sensitive, -		Language:            a.Language, -		Note:                a.Note, -		Fields:              mastoAccount.Fields, -		FollowRequestsCount: frc, +	// check if we have a local account -- if so we can check the user for that account in the DB +	if requestingAccount.Domain == "" { +		requestingUser := >smodel.User{} +		if err := ps.conn.Model(requestingUser).Where("account_id = ?", requestingAccount.ID).Select(); err != nil { +			// if the requesting account is local but doesn't have a corresponding user in the db this is a problem +			if err == pg.ErrNoRows { +				l.Debug("requesting account is local but there's no corresponding user") +				return false, nil +			} else { +				l.Debugf("requesting account is local but there was an error getting the corresponding user: %s", err) +				return false, err +			} +		} +		// okay, user exists, so make sure it has full privileges/is confirmed/approved +		if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() { +			l.Debug("requesting account is local but corresponding user is either disabled, not approved, or not confirmed") +			return false, nil +		}  	} -	return mastoAccount, nil -} +	// if the target status belongs to the requesting account, they should always be able to view it at this point +	if targetStatus.AccountID == requestingAccount.ID { +		return true, nil +	} + +	// At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou +	// First check if a block exists directly between the target account (which authored the status) and the requesting account. +	if blocked, err := ps.Blocked(targetAccount.ID, requestingAccount.ID); err != nil { +		l.Debugf("something went wrong figuring out if the accounts have a block: %s", err) +		return false, err +	} else if blocked { +		// don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please +		l.Debug("a block exists between requesting account and target account") +		return false, nil +	} + +	// check other accounts mentioned/boosted by/replied to by the status, if they exist +	if relevantAccounts != nil { +		// status replies to account id +		if relevantAccounts.ReplyToAccount != nil { +			if blocked, err := ps.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil { +				return false, err +			} else if blocked { +				return false, nil +			} +		} -func (ps *postgresService) AccountToMastoPublic(a *model.Account) (*mastotypes.Account, error) { -	// count followers -	followers := []model.Follow{} -	if err := ps.GetFollowersByAccountID(a.ID, &followers); err != nil { -		if _, ok := err.(ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting followers: %s", err) +		// status boosts accounts id +		if relevantAccounts.BoostedAccount != nil { +			if blocked, err := ps.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil { +				return false, err +			} else if blocked { +				return false, nil +			} +		} + +		// status boosts a reply to account id +		if relevantAccounts.BoostedReplyToAccount != nil { +			if blocked, err := ps.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil { +				return false, err +			} else if blocked { +				return false, nil +			} +		} + +		// status mentions accounts +		for _, a := range relevantAccounts.MentionedAccounts { +			if blocked, err := ps.Blocked(a.ID, requestingAccount.ID); err != nil { +				return false, err +			} else if blocked { +				return false, nil +			} +		} +	} + +	// at this point we know neither account blocks the other, or another account mentioned or otherwise referred to in the status +	// that means it's now just a matter of checking the visibility settings of the status itself +	switch targetStatus.Visibility { +	case gtsmodel.VisibilityPublic, gtsmodel.VisibilityUnlocked: +		// no problem here, just return OK +		return true, nil +	case gtsmodel.VisibilityFollowersOnly: +		// check one-way follow +		follows, err := ps.Follows(requestingAccount, targetAccount) +		if err != nil { +			return false, err +		} +		if !follows { +			return false, nil +		} +		return true, nil +	case gtsmodel.VisibilityMutualsOnly: +		// check mutual follow +		mutuals, err := ps.Mutuals(requestingAccount, targetAccount) +		if err != nil { +			return false, err +		} +		if !mutuals { +			return false, nil +		} +		return true, nil +	case gtsmodel.VisibilityDirect: +		// make sure the requesting account is mentioned in the status +		for _, menchie := range targetStatus.Mentions { +			if menchie == requestingAccount.ID { +				return true, nil // yep it's mentioned! +			}  		} +		return false, nil // it's not mentioned -_-  	} -	var followersCount int -	if followers != nil { -		followersCount = len(followers) + +	return false, errors.New("reached the end of StatusVisible with no result") +} + +func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) { +	return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() +} + +func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) { +	// make sure account 1 follows account 2 +	f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists() +	if err != nil { +		if err == pg.ErrNoRows { +			return false, nil +		} else { +			return false, err +		}  	} -	// count following -	following := []model.Follow{} -	if err := ps.GetFollowingByAccountID(a.ID, &following); err != nil { -		if _, ok := err.(ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting following: %s", err) +	// make sure account 2 follows account 1 +	f2, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account2.ID).Where("target_account_id = ?", account1.ID).Exists() +	if err != nil { +		if err == pg.ErrNoRows { +			return false, nil +		} else { +			return false, err  		}  	} -	var followingCount int -	if following != nil { -		followingCount = len(following) + +	return f1 && f2, nil +} + +func (ps *postgresService) PullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error) { +	accounts := >smodel.RelevantAccounts{ +		MentionedAccounts: []*gtsmodel.Account{},  	} -	// count statuses -	statuses := []model.Status{} -	if err := ps.GetStatusesByAccountID(a.ID, &statuses); err != nil { -		if _, ok := err.(ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting last statuses: %s", err) +	// get the replied to account from the status and add it to the pile +	if targetStatus.InReplyToAccountID != "" { +		repliedToAccount := >smodel.Account{} +		if err := ps.conn.Model(repliedToAccount).Where("id = ?", targetStatus.InReplyToAccountID).Select(); err != nil { +			return accounts, err  		} +		accounts.ReplyToAccount = repliedToAccount  	} -	var statusesCount int -	if statuses != nil { -		statusesCount = len(statuses) + +	// get the boosted account from the status and add it to the pile +	if targetStatus.BoostOfID != "" { +		// retrieve the boosted status first +		boostedStatus := >smodel.Status{} +		if err := ps.conn.Model(boostedStatus).Where("id = ?", targetStatus.BoostOfID).Select(); err != nil { +			return accounts, err +		} +		boostedAccount := >smodel.Account{} +		if err := ps.conn.Model(boostedAccount).Where("id = ?", boostedStatus.AccountID).Select(); err != nil { +			return accounts, err +		} +		accounts.BoostedAccount = boostedAccount + +		// the boosted status might be a reply to another account so we should get that too +		if boostedStatus.InReplyToAccountID != "" { +			boostedStatusRepliedToAccount := >smodel.Account{} +			if err := ps.conn.Model(boostedStatusRepliedToAccount).Where("id = ?", boostedStatus.InReplyToAccountID).Select(); err != nil { +				return accounts, err +			} +			accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount +		}  	} -	// check when the last status was -	lastStatus := &model.Status{} -	if err := ps.GetLastStatusForAccountID(a.ID, lastStatus); err != nil { -		if _, ok := err.(ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting last status: %s", err) +	// now get all accounts with IDs that are mentioned in the status +	for _, mentionedAccountID := range targetStatus.Mentions { +		mentionedAccount := >smodel.Account{} +		if err := ps.conn.Model(mentionedAccount).Where("id = ?", mentionedAccountID).Select(); err != nil { +			return accounts, err  		} +		accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount) +	} + +	return accounts, nil +} + +func (ps *postgresService) GetReplyCountForStatus(status *gtsmodel.Status) (int, error) { +	return ps.conn.Model(>smodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count() +} + +func (ps *postgresService) GetReblogCountForStatus(status *gtsmodel.Status) (int, error) { +	return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Count() +} + +func (ps *postgresService) GetFaveCountForStatus(status *gtsmodel.Status) (int, error) { +	return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Count() +} + +func (ps *postgresService) StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) { +	return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (ps *postgresService) StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) { +	return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (ps *postgresService) StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) { +	return ps.conn.Model(>smodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (ps *postgresService) StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) { +	return ps.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (ps *postgresService) StatusPinnedBy(status *gtsmodel.Status, accountID string) (bool, error) { +	return ps.conn.Model(>smodel.StatusPin{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() +} + +func (ps *postgresService) FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { +	// first check if a fave already exists, we can just return if so +	existingFave := >smodel.StatusFave{} +	err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() +	if err == nil { +		// fave already exists so just return nothing at all +		return nil, nil +	} + +	// an error occurred so it might exist or not, we don't know +	if err != pg.ErrNoRows { +		return nil, err  	} -	var lastStatusAt string -	if lastStatus != nil { -		lastStatusAt = lastStatus.CreatedAt.Format(time.RFC3339) + +	// it doesn't exist so create it +	newFave := >smodel.StatusFave{ +		AccountID:       accountID, +		TargetAccountID: status.AccountID, +		StatusID:        status.ID, +	} +	if _, err = ps.conn.Model(newFave).Insert(); err != nil { +		return nil, err +	} + +	return newFave, nil +} + +func (ps *postgresService) UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { +	// if a fave doesn't exist, we don't need to do anything +	existingFave := >smodel.StatusFave{} +	err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() +	// the fave doesn't exist so return nothing at all +	if err == pg.ErrNoRows { +		return nil, nil +	} + +	// an error occurred so it might exist or not, we don't know +	if err != nil && err != pg.ErrNoRows { +		return nil, err +	} + +	// the fave exists so remove it +	if _, err = ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Delete(); err != nil { +		return nil, err  	} -	// build the avatar and header URLs -	avi := &model.MediaAttachment{} -	if err := ps.GetAvatarForAccountID(avi, a.ID); err != nil { -		if _, ok := err.(ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting avatar: %s", err) +	return existingFave, nil +} + +func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) { +	accounts := []*gtsmodel.Account{} + +	faves := []*gtsmodel.StatusFave{} +	if err := ps.conn.Model(&faves).Where("status_id = ?", status.ID).Select(); err != nil { +		if err == pg.ErrNoRows { +			return accounts, nil // no rows just means nobody has faved this status, so that's fine  		} +		return nil, err // an actual error has occurred  	} -	aviURL := avi.File.Path -	aviURLStatic := avi.Thumbnail.Path -	header := &model.MediaAttachment{} -	if err := ps.GetHeaderForAccountID(avi, a.ID); err != nil { -		if _, ok := err.(ErrNoEntries); !ok { -			return nil, fmt.Errorf("error getting header: %s", err) +	for _, f := range faves { +		acc := >smodel.Account{} +		if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil { +			if err == pg.ErrNoRows { +				continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it +			} +			return nil, err // an actual error has occurred  		} +		accounts = append(accounts, acc)  	} -	headerURL := header.File.Path -	headerURLStatic := header.Thumbnail.Path +	return accounts, nil +} + +/* +	CONVERSION FUNCTIONS +*/ -	// get the fields set on this account -	fields := []mastotypes.Field{} -	for _, f := range a.Fields { -		mField := mastotypes.Field{ -			Name:  f.Name, -			Value: f.Value, +func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) { +	menchies := []*gtsmodel.Mention{} +	for _, a := range targetAccounts { +		// A mentioned account looks like "@test@example.org" or just "@test" for a local account +		// -- we can guarantee this from the regex that targetAccounts should have been derived from. +		// But we still need to do a bit of fiddling to get what we need here -- the username and domain (if given). + +		// 1.  trim off the first @ +		t := strings.TrimPrefix(a, "@") + +		// 2. split the username and domain +		s := strings.Split(t, "@") + +		// 3. if it's length 1 it's a local account, length 2 means remote, anything else means something is wrong +		var local bool +		switch len(s) { +		case 1: +			local = true +		case 2: +			local = false +		default: +			return nil, fmt.Errorf("mentioned account format '%s' was not valid", a)  		} -		if !f.VerifiedAt.IsZero() { -			mField.VerifiedAt = f.VerifiedAt.Format(time.RFC3339) + +		var username, domain string +		username = s[0] +		if !local { +			domain = s[1] +		} + +		// 4. check we now have a proper username and domain +		if username == "" || (!local && domain == "") { +			return nil, fmt.Errorf("username or domain for '%s' was nil", a)  		} -		fields = append(fields, mField) + +		// okay we're good now, we can start pulling accounts out of the database +		mentionedAccount := >smodel.Account{} +		var err error +		if local { +			// local user -- should have a null domain +			err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select() +		} else { +			// remote user -- should have domain defined +			err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? = ?", pg.Ident("domain"), domain).Select() +		} + +		if err != nil { +			if err == pg.ErrNoRows { +				// no result found for this username/domain so just don't include it as a mencho and carry on about our business +				ps.log.Debugf("no account found with username '%s' and domain '%s', skipping it", username, domain) +				continue +			} +			// a serious error has happened so bail +			return nil, fmt.Errorf("error getting account with username '%s' and domain '%s': %s", username, domain, err) +		} + +		// id, createdAt and updatedAt will be populated by the db, so we have everything we need! +		menchies = append(menchies, >smodel.Mention{ +			StatusID:        statusID, +			OriginAccountID: originAccountID, +			TargetAccountID: mentionedAccount.ID, +		})  	} +	return menchies, nil +} -	var acct string -	if a.Domain != "" { -		// this is a remote user -		acct = fmt.Sprintf("%s@%s", a.Username, a.Domain) -	} else { -		// this is a local user -		acct = a.Username -	} - -	return &mastotypes.Account{ -		ID:             a.ID, -		Username:       a.Username, -		Acct:           acct, -		DisplayName:    a.DisplayName, -		Locked:         a.Locked, -		Bot:            a.Bot, -		CreatedAt:      a.CreatedAt.Format(time.RFC3339), -		Note:           a.Note, -		URL:            a.URL, -		Avatar:         aviURL, -		AvatarStatic:   aviURLStatic, -		Header:         headerURL, -		HeaderStatic:   headerURLStatic, -		FollowersCount: followersCount, -		FollowingCount: followingCount, -		StatusesCount:  statusesCount, -		LastStatusAt:   lastStatusAt, -		Emojis:         nil, // TODO: implement this -		Fields:         fields, -	}, nil +func (ps *postgresService) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) { +	newTags := []*gtsmodel.Tag{} +	for _, t := range tags { +		tag := >smodel.Tag{} +		// we can use selectorinsert here to create the new tag if it doesn't exist already +		// inserted will be true if this is a new tag we just created +		if err := ps.conn.Model(tag).Where("name = ?", t).Select(); err != nil { +			if err == pg.ErrNoRows { +				// tag doesn't exist yet so populate it +				tag.ID = uuid.NewString() +				tag.Name = t +				tag.FirstSeenFromAccountID = originAccountID +				tag.CreatedAt = time.Now() +				tag.UpdatedAt = time.Now() +				tag.Useable = true +				tag.Listable = true +			} else { +				return nil, fmt.Errorf("error getting tag with name %s: %s", t, err) +			} +		} + +		// bail already if the tag isn't useable +		if !tag.Useable { +			continue +		} +		tag.LastStatusAt = time.Now() +		newTags = append(newTags, tag) +	} +	return newTags, nil +} + +func (ps *postgresService) EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error) { +	newEmojis := []*gtsmodel.Emoji{} +	for _, e := range emojis { +		emoji := >smodel.Emoji{} +		err := ps.conn.Model(emoji).Where("shortcode = ?", e).Where("visible_in_picker = true").Where("disabled = false").Select() +		if err != nil { +			if err == pg.ErrNoRows { +				// no result found for this username/domain so just don't include it as an emoji and carry on about our business +				ps.log.Debugf("no emoji found with shortcode %s, skipping it", e) +				continue +			} +			// a serious error has happened so bail +			return nil, fmt.Errorf("error getting emoji with shortcode %s: %s", e, err) +		} +		newEmojis = append(newEmojis, emoji) +	} +	return newEmojis, nil  }  | 
