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:
@ -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,
|
||||
|
@ -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.
|
127
internal/db/gtsmodel/activitystreams.go
Normal file
127
internal/db/gtsmodel/activitystreams.go
Normal 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"
|
||||
)
|
@ -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,
|
||||
}
|
||||
}
|
19
internal/db/gtsmodel/block.go
Normal file
19
internal/db/gtsmodel/block.go
Normal 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
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package model
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
|
@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package model
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
|
74
internal/db/gtsmodel/emoji.go
Normal file
74
internal/db/gtsmodel/emoji.go
Normal 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
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package model
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
|
@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package model
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
|
@ -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
|
||||
}
|
39
internal/db/gtsmodel/mention.go
Normal file
39
internal/db/gtsmodel/mention.go
Normal 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
|
||||
}
|
19
internal/db/gtsmodel/poll.go
Normal file
19
internal/db/gtsmodel/poll.go
Normal 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
|
138
internal/db/gtsmodel/status.go
Normal file
138
internal/db/gtsmodel/status.go
Normal 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
|
||||
}
|
35
internal/db/gtsmodel/statusbookmark.go
Normal file
35
internal/db/gtsmodel/statusbookmark.go
Normal 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"`
|
||||
}
|
38
internal/db/gtsmodel/statusfave.go
Normal file
38
internal/db/gtsmodel/statusfave.go
Normal 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:"-"`
|
||||
}
|
35
internal/db/gtsmodel/statusmute.go
Normal file
35
internal/db/gtsmodel/statusmute.go
Normal 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"`
|
||||
}
|
33
internal/db/gtsmodel/statuspin.go
Normal file
33
internal/db/gtsmodel/statuspin.go
Normal 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"`
|
||||
}
|
41
internal/db/gtsmodel/tag.go
Normal file
41
internal/db/gtsmodel/tag.go
Normal 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()"`
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package model
|
||||
package gtsmodel
|
||||
|
||||
import (
|
||||
"net"
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -34,11 +34,11 @@ import (
|
||||
"github.com/go-pg/pg/extra/pgdebug"
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/go-pg/pg/v10/orm"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/pkg/mastotypes"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@ -60,12 +60,6 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry
|
||||
}
|
||||
log.Debugf("using pg options: %+v", opts)
|
||||
|
||||
readyChan := make(chan interface{})
|
||||
opts.OnConnect = func(ctx context.Context, c *pg.Conn) error {
|
||||
close(readyChan)
|
||||
return nil
|
||||
}
|
||||
|
||||
// create a connection
|
||||
pgCtx, cancel := context.WithCancel(ctx)
|
||||
conn := pg.Connect(opts).WithContext(pgCtx)
|
||||
@ -80,8 +74,7 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry
|
||||
})
|
||||
}
|
||||
|
||||
// actually *begin* the connection so that we can tell if the db is there
|
||||
// and listening, and also trigger the opts.OnConnect function passed in above
|
||||
// actually *begin* the connection so that we can tell if the db is there and listening
|
||||
if err := conn.Ping(ctx); err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("db connection error: %s", err)
|
||||
@ -95,16 +88,6 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry
|
||||
}
|
||||
log.Infof("connected to postgres version: %s", version)
|
||||
|
||||
// make sure the opts.OnConnect function has been triggered
|
||||
// and closed the ready channel
|
||||
select {
|
||||
case <-readyChan:
|
||||
log.Infof("postgres connection ready")
|
||||
case <-time.After(5 * time.Second):
|
||||
cancel()
|
||||
return nil, errors.New("db connection timeout")
|
||||
}
|
||||
|
||||
ps := &postgresService{
|
||||
config: c,
|
||||
conn: conn,
|
||||
@ -214,9 +197,9 @@ func (ps *postgresService) IsHealthy(ctx context.Context) error {
|
||||
|
||||
func (ps *postgresService) CreateSchema(ctx context.Context) error {
|
||||
models := []interface{}{
|
||||
(*model.Account)(nil),
|
||||
(*model.Status)(nil),
|
||||
(*model.User)(nil),
|
||||
(*gtsmodel.Account)(nil),
|
||||
(*gtsmodel.Status)(nil),
|
||||
(*gtsmodel.User)(nil),
|
||||
}
|
||||
ps.log.Info("creating db schema")
|
||||
|
||||
@ -254,6 +237,10 @@ func (ps *postgresService) GetWhere(key string, value interface{}, i interface{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// func (ps *postgresService) GetWhereMany(i interface{}, where ...model.Where) error {
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (ps *postgresService) GetAll(i interface{}) error {
|
||||
if err := ps.conn.Model(i).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
@ -269,8 +256,18 @@ func (ps *postgresService) Put(i interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (ps *postgresService) Upsert(i interface{}, conflictColumn string) error {
|
||||
if _, err := ps.conn.Model(i).OnConflict(fmt.Sprintf("(%s) DO UPDATE", conflictColumn)).Insert(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) UpdateByID(id string, i interface{}) error {
|
||||
if _, err := ps.conn.Model(i).OnConflict("(id) DO UPDATE").Insert(); err != nil {
|
||||
if _, err := ps.conn.Model(i).Where("id = ?", id).OnConflict("(id) DO UPDATE").Insert(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return ErrNoEntries{}
|
||||
}
|
||||
@ -308,8 +305,25 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac
|
||||
HANDY SHORTCUTS
|
||||
*/
|
||||
|
||||
func (ps *postgresService) GetAccountByUserID(userID string, account *model.Account) error {
|
||||
user := &model.User{
|
||||
func (ps *postgresService) CreateInstanceAccount() error {
|
||||
username := ps.config.Host
|
||||
instanceAccount := >smodel.Account{
|
||||
Username: username,
|
||||
}
|
||||
inserted, err := ps.conn.Model(instanceAccount).Where("username = ?", username).SelectOrInsert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inserted {
|
||||
ps.log.Infof("created instance account %s with id %s", username, instanceAccount.ID)
|
||||
} else {
|
||||
ps.log.Infof("instance account %s already exists with id %s", username, instanceAccount.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.Account) error {
|
||||
user := >smodel.User{
|
||||
ID: userID,
|
||||
}
|
||||
if err := ps.conn.Model(user).Where("id = ?", userID).Select(); err != nil {
|
||||
@ -327,7 +341,7 @@ func (ps *postgresService) GetAccountByUserID(userID string, account *model.Acco
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, followRequests *[]model.FollowRequest) error {
|
||||
func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error {
|
||||
if err := ps.conn.Model(followRequests).Where("target_account_id = ?", accountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return ErrNoEntries{}
|
||||
@ -337,7 +351,7 @@ func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, follo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetFollowingByAccountID(accountID string, following *[]model.Follow) error {
|
||||
func (ps *postgresService) GetFollowingByAccountID(accountID string, following *[]gtsmodel.Follow) error {
|
||||
if err := ps.conn.Model(following).Where("account_id = ?", accountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return ErrNoEntries{}
|
||||
@ -347,7 +361,7 @@ func (ps *postgresService) GetFollowingByAccountID(accountID string, following *
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]model.Follow) error {
|
||||
func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error {
|
||||
if err := ps.conn.Model(followers).Where("target_account_id = ?", accountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return ErrNoEntries{}
|
||||
@ -357,7 +371,7 @@ func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetStatusesByAccountID(accountID string, statuses *[]model.Status) error {
|
||||
func (ps *postgresService) GetStatusesByAccountID(accountID string, statuses *[]gtsmodel.Status) error {
|
||||
if err := ps.conn.Model(statuses).Where("account_id = ?", accountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return ErrNoEntries{}
|
||||
@ -367,7 +381,7 @@ func (ps *postgresService) GetStatusesByAccountID(accountID string, statuses *[]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]model.Status, limit int) error {
|
||||
func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int) error {
|
||||
q := ps.conn.Model(statuses).Order("created_at DESC")
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
@ -384,7 +398,7 @@ func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuse
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *model.Status) error {
|
||||
func (ps *postgresService) GetLastStatusForAccountID(accountID string, status *gtsmodel.Status) error {
|
||||
if err := ps.conn.Model(status).Order("created_at DESC").Limit(1).Where("account_id = ?", accountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return ErrNoEntries{}
|
||||
@ -399,7 +413,7 @@ func (ps *postgresService) IsUsernameAvailable(username string) error {
|
||||
// if no error we fail because it means we found something
|
||||
// if error but it's not pg.ErrNoRows then we fail
|
||||
// if err is pg.ErrNoRows we're good, we found nothing so continue
|
||||
if err := ps.conn.Model(&model.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil {
|
||||
if err := ps.conn.Model(>smodel.Account{}).Where("username = ?", username).Where("domain = ?", nil).Select(); err == nil {
|
||||
return fmt.Errorf("username %s already in use", username)
|
||||
} else if err != pg.ErrNoRows {
|
||||
return fmt.Errorf("db error: %s", err)
|
||||
@ -416,7 +430,7 @@ func (ps *postgresService) IsEmailAvailable(email string) error {
|
||||
domain := strings.Split(m.Address, "@")[1] // domain will always be the second part after @
|
||||
|
||||
// check if the email domain is blocked
|
||||
if err := ps.conn.Model(&model.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil {
|
||||
if err := ps.conn.Model(>smodel.EmailDomainBlock{}).Where("domain = ?", domain).Select(); err == nil {
|
||||
// fail because we found something
|
||||
return fmt.Errorf("email domain %s is blocked", domain)
|
||||
} else if err != pg.ErrNoRows {
|
||||
@ -425,7 +439,7 @@ func (ps *postgresService) IsEmailAvailable(email string) error {
|
||||
}
|
||||
|
||||
// check if this email is associated with a user already
|
||||
if err := ps.conn.Model(&model.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil {
|
||||
if err := ps.conn.Model(>smodel.User{}).Where("email = ?", email).WhereOr("unconfirmed_email = ?", email).Select(); err == nil {
|
||||
// fail because we found something
|
||||
return fmt.Errorf("email %s already in use", email)
|
||||
} else if err != pg.ErrNoRows {
|
||||
@ -435,7 +449,7 @@ func (ps *postgresService) IsEmailAvailable(email string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*model.User, error) {
|
||||
func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string) (*gtsmodel.User, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
ps.log.Errorf("error creating new rsa key: %s", err)
|
||||
@ -444,19 +458,19 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr
|
||||
|
||||
uris := util.GenerateURIs(username, ps.config.Protocol, ps.config.Host)
|
||||
|
||||
a := &model.Account{
|
||||
a := >smodel.Account{
|
||||
Username: username,
|
||||
DisplayName: username,
|
||||
Reason: reason,
|
||||
URL: uris.UserURL,
|
||||
PrivateKey: key,
|
||||
PublicKey: &key.PublicKey,
|
||||
ActorType: "Person",
|
||||
ActorType: gtsmodel.ActivityStreamsPerson,
|
||||
URI: uris.UserURI,
|
||||
InboxURL: uris.InboxURL,
|
||||
OutboxURL: uris.OutboxURL,
|
||||
FollowersURL: uris.FollowersURL,
|
||||
FeaturedCollectionURL: uris.CollectionURL,
|
||||
InboxURL: uris.InboxURI,
|
||||
OutboxURL: uris.OutboxURI,
|
||||
FollowersURL: uris.FollowersURI,
|
||||
FeaturedCollectionURL: uris.CollectionURI,
|
||||
}
|
||||
if _, err = ps.conn.Model(a).Insert(); err != nil {
|
||||
return nil, err
|
||||
@ -466,7 +480,7 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error hashing password: %s", err)
|
||||
}
|
||||
u := &model.User{
|
||||
u := >smodel.User{
|
||||
AccountID: a.ID,
|
||||
EncryptedPassword: string(pw),
|
||||
SignUpIP: signUpIP,
|
||||
@ -482,13 +496,45 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) SetHeaderOrAvatarForAccountID(mediaAttachment *model.MediaAttachment, accountID string) error {
|
||||
_, err := ps.conn.Model(mediaAttachment).Insert()
|
||||
return err
|
||||
func (ps *postgresService) SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error {
|
||||
if mediaAttachment.Avatar && mediaAttachment.Header {
|
||||
return errors.New("one media attachment cannot be both header and avatar")
|
||||
}
|
||||
|
||||
var headerOrAVI string
|
||||
if mediaAttachment.Avatar {
|
||||
headerOrAVI = "avatar"
|
||||
} else if mediaAttachment.Header {
|
||||
headerOrAVI = "header"
|
||||
} else {
|
||||
return errors.New("given media attachment was neither a header nor an avatar")
|
||||
}
|
||||
|
||||
// TODO: there are probably more side effects here that need to be handled
|
||||
if _, err := ps.conn.Model(mediaAttachment).OnConflict("(id) DO UPDATE").Insert(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ps.conn.Model(>smodel.Account{}).Set(fmt.Sprintf("%s_media_attachment_id = ?", headerOrAVI), mediaAttachment.ID).Where("id = ?", accountID).Update(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetHeaderForAccountID(header *model.MediaAttachment, accountID string) error {
|
||||
if err := ps.conn.Model(header).Where("account_id = ?", accountID).Where("header = ?", true).Select(); err != nil {
|
||||
func (ps *postgresService) GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error {
|
||||
acct := >smodel.Account{}
|
||||
if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if acct.HeaderMediaAttachmentID == "" {
|
||||
return ErrNoEntries{}
|
||||
}
|
||||
|
||||
if err := ps.conn.Model(header).Where("id = ?", acct.HeaderMediaAttachmentID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return ErrNoEntries{}
|
||||
}
|
||||
@ -497,8 +543,20 @@ func (ps *postgresService) GetHeaderForAccountID(header *model.MediaAttachment,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetAvatarForAccountID(avatar *model.MediaAttachment, accountID string) error {
|
||||
if err := ps.conn.Model(avatar).Where("account_id = ?", accountID).Where("avatar = ?", true).Select(); err != nil {
|
||||
func (ps *postgresService) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error {
|
||||
acct := >smodel.Account{}
|
||||
if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if acct.AvatarMediaAttachmentID == "" {
|
||||
return ErrNoEntries{}
|
||||
}
|
||||
|
||||
if err := ps.conn.Model(avatar).Where("id = ?", acct.AvatarMediaAttachmentID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return ErrNoEntries{}
|
||||
}
|
||||
@ -507,156 +565,480 @@ func (ps *postgresService) GetAvatarForAccountID(avatar *model.MediaAttachment,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) {
|
||||
var blocked bool
|
||||
if err := ps.conn.Model(>smodel.Block{}).
|
||||
Where("account_id = ?", account1).Where("target_account_id = ?", account2).
|
||||
WhereOr("target_account_id = ?", account1).Where("account_id = ?", account2).
|
||||
Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
blocked = false
|
||||
return blocked, nil
|
||||
} else {
|
||||
return blocked, err
|
||||
}
|
||||
}
|
||||
blocked = true
|
||||
return blocked, nil
|
||||
}
|
||||
|
||||
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 := >smodel.User{}
|
||||
if err := ps.conn.Model(targetUser).Where("account_id = ?", targetAccount.ID).Select(); err != nil {
|
||||
l.Debug("target user could not be selected")
|
||||
if err == pg.ErrNoRows {
|
||||
return false, ErrNoEntries{}
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// 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 := >smodel.User{}
|
||||
if err := ps.conn.Model(requestingUser).Where("account_id = ?", requestingAccount.ID).Select(); err != nil {
|
||||
// if the requesting account is local but doesn't have a corresponding user in the db this is a problem
|
||||
if err == pg.ErrNoRows {
|
||||
l.Debug("requesting account is local but there's no corresponding user")
|
||||
return false, nil
|
||||
} else {
|
||||
l.Debugf("requesting account is local but there was an error getting the corresponding user: %s", err)
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
// okay, user exists, so make sure it has full privileges/is confirmed/approved
|
||||
if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() {
|
||||
l.Debug("requesting account is local but corresponding user is either disabled, not approved, or not confirmed")
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) {
|
||||
// make sure account 1 follows account 2
|
||||
f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists()
|
||||
if err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return false, nil
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// make sure account 2 follows account 1
|
||||
f2, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account2.ID).Where("target_account_id = ?", account1.ID).Exists()
|
||||
if err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return false, nil
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return f1 && f2, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) PullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error) {
|
||||
accounts := >smodel.RelevantAccounts{
|
||||
MentionedAccounts: []*gtsmodel.Account{},
|
||||
}
|
||||
|
||||
// get the replied to account from the status and add it to the pile
|
||||
if targetStatus.InReplyToAccountID != "" {
|
||||
repliedToAccount := >smodel.Account{}
|
||||
if err := ps.conn.Model(repliedToAccount).Where("id = ?", targetStatus.InReplyToAccountID).Select(); err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
accounts.ReplyToAccount = repliedToAccount
|
||||
}
|
||||
|
||||
// get the boosted account from the status and add it to the pile
|
||||
if targetStatus.BoostOfID != "" {
|
||||
// retrieve the boosted status first
|
||||
boostedStatus := >smodel.Status{}
|
||||
if err := ps.conn.Model(boostedStatus).Where("id = ?", targetStatus.BoostOfID).Select(); err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
boostedAccount := >smodel.Account{}
|
||||
if err := ps.conn.Model(boostedAccount).Where("id = ?", boostedStatus.AccountID).Select(); err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
accounts.BoostedAccount = boostedAccount
|
||||
|
||||
// the boosted status might be a reply to another account so we should get that too
|
||||
if boostedStatus.InReplyToAccountID != "" {
|
||||
boostedStatusRepliedToAccount := >smodel.Account{}
|
||||
if err := ps.conn.Model(boostedStatusRepliedToAccount).Where("id = ?", boostedStatus.InReplyToAccountID).Select(); err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount
|
||||
}
|
||||
}
|
||||
|
||||
// now get all accounts with IDs that are mentioned in the status
|
||||
for _, mentionedAccountID := range targetStatus.Mentions {
|
||||
mentionedAccount := >smodel.Account{}
|
||||
if err := ps.conn.Model(mentionedAccount).Where("id = ?", mentionedAccountID).Select(); err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount)
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetReplyCountForStatus(status *gtsmodel.Status) (int, error) {
|
||||
return ps.conn.Model(>smodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count()
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetReblogCountForStatus(status *gtsmodel.Status) (int, error) {
|
||||
return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Count()
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetFaveCountForStatus(status *gtsmodel.Status) (int, error) {
|
||||
return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Count()
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
return ps.conn.Model(>smodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
return ps.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusPinnedBy(status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
return ps.conn.Model(>smodel.StatusPin{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) {
|
||||
// first check if a fave already exists, we can just return if so
|
||||
existingFave := >smodel.StatusFave{}
|
||||
err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select()
|
||||
if err == nil {
|
||||
// fave already exists so just return nothing at all
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// an error occurred so it might exist or not, we don't know
|
||||
if err != pg.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// it doesn't exist so create it
|
||||
newFave := >smodel.StatusFave{
|
||||
AccountID: accountID,
|
||||
TargetAccountID: status.AccountID,
|
||||
StatusID: status.ID,
|
||||
}
|
||||
if _, err = ps.conn.Model(newFave).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newFave, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) {
|
||||
// if a fave doesn't exist, we don't need to do anything
|
||||
existingFave := >smodel.StatusFave{}
|
||||
err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select()
|
||||
// the fave doesn't exist so return nothing at all
|
||||
if err == pg.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// an error occurred so it might exist or not, we don't know
|
||||
if err != nil && err != pg.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// the fave exists so remove it
|
||||
if _, err = ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Delete(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 := >smodel.Account{}
|
||||
if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it
|
||||
}
|
||||
return nil, err // an actual error has occurred
|
||||
}
|
||||
accounts = append(accounts, acc)
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
/*
|
||||
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 := >smodel.Account{}
|
||||
var err error
|
||||
if local {
|
||||
// local user -- should have a null domain
|
||||
err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select()
|
||||
} else {
|
||||
// remote user -- should have domain defined
|
||||
err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? = ?", pg.Ident("domain"), domain).Select()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
// no result found for this username/domain so just don't include it as a mencho and carry on about our business
|
||||
ps.log.Debugf("no account found with username '%s' and domain '%s', skipping it", username, domain)
|
||||
continue
|
||||
}
|
||||
// a serious error has happened so bail
|
||||
return nil, fmt.Errorf("error getting account with username '%s' and domain '%s': %s", username, domain, err)
|
||||
}
|
||||
|
||||
// id, createdAt and updatedAt will be populated by the db, so we have everything we need!
|
||||
menchies = append(menchies, >smodel.Mention{
|
||||
StatusID: statusID,
|
||||
OriginAccountID: originAccountID,
|
||||
TargetAccountID: mentionedAccount.ID,
|
||||
})
|
||||
}
|
||||
return menchies, nil
|
||||
}
|
||||
|
||||
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 := >smodel.Tag{}
|
||||
// we can use selectorinsert here to create the new tag if it doesn't exist already
|
||||
// inserted will be true if this is a new tag we just created
|
||||
if err := ps.conn.Model(tag).Where("name = ?", t).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
// tag doesn't exist yet so populate it
|
||||
tag.ID = uuid.NewString()
|
||||
tag.Name = t
|
||||
tag.FirstSeenFromAccountID = originAccountID
|
||||
tag.CreatedAt = time.Now()
|
||||
tag.UpdatedAt = time.Now()
|
||||
tag.Useable = true
|
||||
tag.Listable = true
|
||||
} else {
|
||||
return nil, fmt.Errorf("error getting tag with name %s: %s", t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
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 := >smodel.Emoji{}
|
||||
err := ps.conn.Model(emoji).Where("shortcode = ?", e).Where("visible_in_picker = true").Where("disabled = false").Select()
|
||||
if err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
// no result found for this username/domain so just don't include it as an emoji and carry on about our business
|
||||
ps.log.Debugf("no emoji found with shortcode %s, skipping it", e)
|
||||
continue
|
||||
}
|
||||
// a serious error has happened so bail
|
||||
return nil, fmt.Errorf("error getting emoji with shortcode %s: %s", e, err)
|
||||
}
|
||||
newEmojis = append(newEmojis, emoji)
|
||||
}
|
||||
return newEmojis, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user