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 } |