Api/v1/statuses (#11)

This PR adds:
Statuses

    New status creation.
    View existing status
    Delete a status
    Fave a status
    Unfave a status
    See who's faved a status

Media

    Upload media attachment and store/retrieve it
    Upload custom emoji and store/retrieve it

Fileserver

    Serve files from storage

Testing

    Test models, testrig -- run a GTS test instance and play around with it.
This commit is contained in:
Tobi Smethurst
2021-04-19 19:42:19 +02:00
committed by GitHub
parent 71a49e2b43
commit 32c5fd987a
150 changed files with 9023 additions and 788 deletions

View File

@ -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)
// 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)
// 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)
// 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,

View File

@ -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.

View File

@ -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"
)

View File

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

View File

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

View File

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package model
package gtsmodel
import "time"

View File

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package model
package gtsmodel
import "time"

View File

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

View File

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package model
package gtsmodel
import "time"

View File

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package model
package gtsmodel
import "time"

View File

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

View File

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

View File

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

View File

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

View File

@ -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"`
}

View File

@ -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:"-"`
}

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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()"`
}

View File

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package model
package gtsmodel
import (
"net"

View File

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

View File

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

View File

@ -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 := &gtsmodel.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 := &gtsmodel.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(&gtsmodel.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(&gtsmodel.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(&gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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(&gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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
}
func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) {
var blocked bool
if err := ps.conn.Model(&gtsmodel.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
}
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
}
// if the target user doesn't exist (anymore) then the status also shouldn't be visible
targetUser := &gtsmodel.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
}
}
// 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
}
// 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
}
// check if we have a local account -- if so we can check the user for that account in the DB
if requestingAccount.Domain == "" {
requestingUser := &gtsmodel.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
}
}
// 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
}
}
// 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 -_-
}
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(&gtsmodel.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(&gtsmodel.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
}
}
// make sure account 2 follows account 1
f2, err := ps.conn.Model(&gtsmodel.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
}
}
return f1 && f2, nil
}
func (ps *postgresService) PullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error) {
accounts := &gtsmodel.RelevantAccounts{
MentionedAccounts: []*gtsmodel.Account{},
}
// get the replied to account from the status and add it to the pile
if targetStatus.InReplyToAccountID != "" {
repliedToAccount := &gtsmodel.Account{}
if err := ps.conn.Model(repliedToAccount).Where("id = ?", targetStatus.InReplyToAccountID).Select(); err != nil {
return accounts, err
}
accounts.ReplyToAccount = repliedToAccount
}
// get the boosted account from the status and add it to the pile
if targetStatus.BoostOfID != "" {
// retrieve the boosted status first
boostedStatus := &gtsmodel.Status{}
if err := ps.conn.Model(boostedStatus).Where("id = ?", targetStatus.BoostOfID).Select(); err != nil {
return accounts, err
}
boostedAccount := &gtsmodel.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 := &gtsmodel.Account{}
if err := ps.conn.Model(boostedStatusRepliedToAccount).Where("id = ?", boostedStatus.InReplyToAccountID).Select(); err != nil {
return accounts, err
}
accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount
}
}
// now get all accounts with IDs that are mentioned in the status
for _, mentionedAccountID := range targetStatus.Mentions {
mentionedAccount := &gtsmodel.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(&gtsmodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count()
}
func (ps *postgresService) GetReblogCountForStatus(status *gtsmodel.Status) (int, error) {
return ps.conn.Model(&gtsmodel.Status{}).Where("boost_of_id = ?", status.ID).Count()
}
func (ps *postgresService) GetFaveCountForStatus(status *gtsmodel.Status) (int, error) {
return ps.conn.Model(&gtsmodel.StatusFave{}).Where("status_id = ?", status.ID).Count()
}
func (ps *postgresService) StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) {
return ps.conn.Model(&gtsmodel.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(&gtsmodel.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(&gtsmodel.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(&gtsmodel.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(&gtsmodel.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 := &gtsmodel.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
}
// it doesn't exist so create it
newFave := &gtsmodel.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 := &gtsmodel.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(&gtsmodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Delete(); err != nil {
return nil, 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
}
for _, f := range faves {
acc := &gtsmodel.Account{}
if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil {
if err == pg.ErrNoRows {
continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it
}
return nil, err // an actual error has occurred
}
accounts = append(accounts, acc)
}
return accounts, nil
}
/*
CONVERSION FUNCTIONS
*/
// 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) 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).
// then adding the Source object to it...
// 1. trim off the first @
t := strings.TrimPrefix(a, "@")
// 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)
// 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)
}
}
var frc int
if fr != nil {
frc = len(fr)
}
mastoAccount.Source = &mastotypes.Source{
Privacy: a.Privacy,
Sensitive: a.Sensitive,
Language: a.Language,
Note: a.Note,
Fields: mastoAccount.Fields,
FollowRequestsCount: frc,
}
var username, domain string
username = s[0]
if !local {
domain = s[1]
}
return mastoAccount, nil
// 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)
}
// okay we're good now, we can start pulling accounts out of the database
mentionedAccount := &gtsmodel.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, &gtsmodel.Mention{
StatusID: statusID,
OriginAccountID: originAccountID,
TargetAccountID: mentionedAccount.ID,
})
}
return menchies, 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)
func (ps *postgresService) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) {
newTags := []*gtsmodel.Tag{}
for _, t := range tags {
tag := &gtsmodel.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)
}
}
}
var followersCount int
if followers != nil {
followersCount = len(followers)
}
// 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)
// bail already if the tag isn't useable
if !tag.Useable {
continue
}
tag.LastStatusAt = time.Now()
newTags = append(newTags, tag)
}
var followingCount int
if following != nil {
followingCount = len(following)
}
// 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)
}
}
var statusesCount int
if statuses != nil {
statusesCount = len(statuses)
}
// 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)
}
}
var lastStatusAt string
if lastStatus != nil {
lastStatusAt = lastStatus.CreatedAt.Format(time.RFC3339)
}
// 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)
}
}
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)
}
}
headerURL := header.File.Path
headerURLStatic := header.Thumbnail.Path
// get the fields set on this account
fields := []mastotypes.Field{}
for _, f := range a.Fields {
mField := mastotypes.Field{
Name: f.Name,
Value: f.Value,
}
if !f.VerifiedAt.IsZero() {
mField.VerifiedAt = f.VerifiedAt.Format(time.RFC3339)
}
fields = append(fields, mField)
}
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
return newTags, nil
}
func (ps *postgresService) EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error) {
newEmojis := []*gtsmodel.Emoji{}
for _, e := range emojis {
emoji := &gtsmodel.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
}