mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-30 00:36:14 -06:00
start moving some database stuff around
This commit is contained in:
parent
ce190d867c
commit
f409f7c65a
43 changed files with 1230 additions and 1223 deletions
65
internal/db/account.go
Normal file
65
internal/db/account.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package db
|
||||
|
||||
import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
||||
type Account interface {
|
||||
// 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 *gtsmodel.Account) error
|
||||
|
||||
// GetLocalAccountByUsername is a shortcut for the common action of fetching an account ON THIS INSTANCE
|
||||
// according to its username, which should be unique.
|
||||
// The given account pointer will be set to the result of the query, whatever it is.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetLocalAccountByUsername(username string, account *gtsmodel.Account) error
|
||||
|
||||
// GetAccountFollowRequests is a shortcut for the common action of fetching a list of follow requests targeting the given account ID.
|
||||
// The given slice 'followRequests' will be set to the result of the query, whatever it is.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetAccountFollowRequests(accountID string, followRequests *[]gtsmodel.FollowRequest) error
|
||||
|
||||
// GetAccountFollowing is a shortcut for the common action of fetching a list of accounts that accountID is following.
|
||||
// The given slice 'following' will be set to the result of the query, whatever it is.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetAccountFollowing(accountID string, following *[]gtsmodel.Follow) error
|
||||
|
||||
// GetAccountFollowers is a shortcut for the common action of fetching a list of accounts that accountID is followed by.
|
||||
// The given slice 'followers' will be set to the result of the query, whatever it is.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
//
|
||||
// If localOnly is set to true, then only followers from *this instance* will be returned.
|
||||
GetAccountFollowers(accountID string, followers *[]gtsmodel.Follow, localOnly bool) error
|
||||
|
||||
// GetAccountFaves is a shortcut for the common action of fetching a list of faves made by the given accountID.
|
||||
// The given slice 'faves' will be set to the result of the query, whatever it is.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFave) error
|
||||
|
||||
// GetAccountStatusesCount is a shortcut for the common action of counting statuses produced by accountID.
|
||||
GetAccountStatusesCount(accountID string) (int, error)
|
||||
|
||||
// GetAccountStatuses is a shortcut for getting the most recent statuses. accountID is optional, if not provided
|
||||
// then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can
|
||||
// be very memory intensive so you probably shouldn't do this!
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetAccountStatuses(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error)
|
||||
|
||||
GetAccountBlocks(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error)
|
||||
|
||||
// GetAccountLastStatus 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
|
||||
GetAccountLastStatus(accountID string, status *gtsmodel.Status) error
|
||||
|
||||
// SetAccountHeaderOrAvatar sets the header or avatar for the given accountID to the given media attachment.
|
||||
SetAccountHeaderOrAvatar(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.
|
||||
GetAccountAvatar(avatar *gtsmodel.MediaAttachment, accountID string) error
|
||||
|
||||
// GetAccountHeader 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.
|
||||
GetAccountHeader(header *gtsmodel.MediaAttachment, accountID string) error
|
||||
}
|
||||
34
internal/db/admin.go
Normal file
34
internal/db/admin.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
type Admin interface {
|
||||
// IsUsernameAvailable checks whether a given username is available on our domain.
|
||||
// Returns an error if the username is already taken, or something went wrong in the db.
|
||||
IsUsernameAvailable(username string) error
|
||||
|
||||
// IsEmailAvailable checks whether a given email address for a new account is available to be used on our domain.
|
||||
// Return an error if:
|
||||
// A) the email is already associated with an account
|
||||
// B) we block signups from this email domain
|
||||
// C) something went wrong in the db
|
||||
IsEmailAvailable(email string) error
|
||||
|
||||
// NewSignup creates a new user in the database with the given parameters.
|
||||
// By the time this function is called, it should be assumed that all the parameters have passed validation!
|
||||
NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, error)
|
||||
|
||||
// CreateInstanceAccount creates an account in the database with the same username as the instance host value.
|
||||
// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'.
|
||||
// This is needed for things like serving files that belong to the instance and not an individual user/account.
|
||||
CreateInstanceAccount() error
|
||||
|
||||
// CreateInstanceInstance creates an instance in the database with the same domain as the instance host value.
|
||||
// Ie., if the instance is hosted at 'example.org' the instance will have a domain of 'example.org'.
|
||||
// This is needed for things like serving instance information through /api/v1/instance
|
||||
CreateInstanceInstance() error
|
||||
}
|
||||
64
internal/db/basic.go
Normal file
64
internal/db/basic.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package db
|
||||
|
||||
import "context"
|
||||
|
||||
type Basic interface {
|
||||
// CreateTable creates a table for the given interface.
|
||||
// For implementations that don't use tables, this can just return nil.
|
||||
CreateTable(i interface{}) error
|
||||
|
||||
// DropTable drops the table for the given interface.
|
||||
// For implementations that don't use tables, this can just return nil.
|
||||
DropTable(i interface{}) error
|
||||
|
||||
// Stop should stop and close the database connection cleanly, returning an error if this is not possible.
|
||||
// If the database implementation doesn't need to be stopped, this can just return nil.
|
||||
Stop(ctx context.Context) error
|
||||
|
||||
// IsHealthy should return nil if the database connection is healthy, or an error if not.
|
||||
IsHealthy(ctx context.Context) error
|
||||
|
||||
// GetByID gets one entry by its id. In a database like postgres, this might be the 'id' field of the entry,
|
||||
// for other implementations (for example, in-memory) it might just be the key of a map.
|
||||
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetByID(id string, i interface{}) error
|
||||
|
||||
// GetWhere gets one entry where key = value. This is similar to GetByID but allows the caller to specify the
|
||||
// name of the key to select from.
|
||||
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetWhere(where []Where, i interface{}) error
|
||||
|
||||
// GetAll will try to get all entries of type i.
|
||||
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetAll(i interface{}) error
|
||||
|
||||
// Put simply stores i. It is up to the implementation to figure out how to store it, and using what key.
|
||||
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
|
||||
Put(i interface{}) 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
|
||||
|
||||
// UpdateOneByID updates interface i with database the given database id. It will update one field of key key and value value.
|
||||
UpdateOneByID(id string, key string, value interface{}, i interface{}) error
|
||||
|
||||
// UpdateWhere updates column key of interface i with the given value, where the given parameters apply.
|
||||
UpdateWhere(where []Where, key string, value interface{}, i interface{}) error
|
||||
|
||||
// DeleteByID removes i with id id.
|
||||
// If i didn't exist anyway, then no error should be returned.
|
||||
DeleteByID(id string, i interface{}) error
|
||||
|
||||
// DeleteWhere deletes i where key = value
|
||||
// If i didn't exist anyway, then no error should be returned.
|
||||
DeleteWhere(where []Where, i interface{}) error
|
||||
}
|
||||
|
|
@ -19,9 +19,6 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
|
|
@ -34,253 +31,14 @@ const (
|
|||
// Note that in all of the functions below, the passed interface should be a pointer or a slice, which will then be populated
|
||||
// by whatever is returned from the database.
|
||||
type DB interface {
|
||||
/*
|
||||
BASIC DB FUNCTIONALITY
|
||||
*/
|
||||
|
||||
// CreateTable creates a table for the given interface.
|
||||
// For implementations that don't use tables, this can just return nil.
|
||||
CreateTable(i interface{}) error
|
||||
|
||||
// DropTable drops the table for the given interface.
|
||||
// For implementations that don't use tables, this can just return nil.
|
||||
DropTable(i interface{}) error
|
||||
|
||||
// Stop should stop and close the database connection cleanly, returning an error if this is not possible.
|
||||
// If the database implementation doesn't need to be stopped, this can just return nil.
|
||||
Stop(ctx context.Context) error
|
||||
|
||||
// IsHealthy should return nil if the database connection is healthy, or an error if not.
|
||||
IsHealthy(ctx context.Context) error
|
||||
|
||||
// GetByID gets one entry by its id. In a database like postgres, this might be the 'id' field of the entry,
|
||||
// for other implementations (for example, in-memory) it might just be the key of a map.
|
||||
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetByID(id string, i interface{}) error
|
||||
|
||||
// GetWhere gets one entry where key = value. This is similar to GetByID but allows the caller to specify the
|
||||
// name of the key to select from.
|
||||
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetWhere(where []Where, i interface{}) error
|
||||
|
||||
// GetAll will try to get all entries of type i.
|
||||
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetAll(i interface{}) error
|
||||
|
||||
// Put simply stores i. It is up to the implementation to figure out how to store it, and using what key.
|
||||
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
|
||||
Put(i interface{}) 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
|
||||
|
||||
// UpdateOneByID updates interface i with database the given database id. It will update one field of key key and value value.
|
||||
UpdateOneByID(id string, key string, value interface{}, i interface{}) error
|
||||
|
||||
// UpdateWhere updates column key of interface i with the given value, where the given parameters apply.
|
||||
UpdateWhere(where []Where, key string, value interface{}, i interface{}) error
|
||||
|
||||
// DeleteByID removes i with id id.
|
||||
// If i didn't exist anyway, then no error should be returned.
|
||||
DeleteByID(id string, i interface{}) error
|
||||
|
||||
// DeleteWhere deletes i where key = value
|
||||
// If i didn't exist anyway, then no error should be returned.
|
||||
DeleteWhere(where []Where, i interface{}) error
|
||||
|
||||
/*
|
||||
HANDY SHORTCUTS
|
||||
*/
|
||||
|
||||
// AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table.
|
||||
// In other words, it should create the follow, and delete the existing follow request.
|
||||
//
|
||||
// It will return the newly created follow for further processing.
|
||||
AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error)
|
||||
|
||||
// CreateInstanceAccount creates an account in the database with the same username as the instance host value.
|
||||
// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'.
|
||||
// This is needed for things like serving files that belong to the instance and not an individual user/account.
|
||||
CreateInstanceAccount() error
|
||||
|
||||
// CreateInstanceInstance creates an instance in the database with the same domain as the instance host value.
|
||||
// Ie., if the instance is hosted at 'example.org' the instance will have a domain of 'example.org'.
|
||||
// This is needed for things like serving instance information through /api/v1/instance
|
||||
CreateInstanceInstance() 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 *gtsmodel.Account) error
|
||||
|
||||
// GetLocalAccountByUsername is a shortcut for the common action of fetching an account ON THIS INSTANCE
|
||||
// according to its username, which should be unique.
|
||||
// The given account pointer will be set to the result of the query, whatever it is.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetLocalAccountByUsername(username string, 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 *[]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 *[]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
|
||||
//
|
||||
// If localOnly is set to true, then only followers from *this instance* will be returned.
|
||||
GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow, localOnly bool) error
|
||||
|
||||
// GetFavesByAccountID is a shortcut for the common action of fetching a list of faves made by the given accountID.
|
||||
// The given slice 'faves' will be set to the result of the query, whatever it is.
|
||||
// In case of no entries, a 'no entries' error will be returned
|
||||
GetFavesByAccountID(accountID string, faves *[]gtsmodel.StatusFave) error
|
||||
|
||||
// CountStatusesByAccountID is a shortcut for the common action of counting statuses produced by accountID.
|
||||
CountStatusesByAccountID(accountID string) (int, error)
|
||||
|
||||
// GetStatusesForAccount 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
|
||||
GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error)
|
||||
|
||||
GetBlocksForAccount(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, 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 *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.
|
||||
IsUsernameAvailable(username string) error
|
||||
|
||||
// IsEmailAvailable checks whether a given email address for a new account is available to be used on our domain.
|
||||
// Return an error if:
|
||||
// A) the email is already associated with an account
|
||||
// B) we block signups from this email domain
|
||||
// C) something went wrong in the db
|
||||
IsEmailAvailable(email string) error
|
||||
|
||||
// NewSignup creates a new user in the database with the given parameters.
|
||||
// By the time this function is called, it should be assumed that all the parameters have passed validation!
|
||||
NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, error)
|
||||
|
||||
// SetHeaderOrAvatarForAccountID sets the header or avatar for the given accountID to the given media attachment.
|
||||
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 *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 *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)
|
||||
|
||||
// GetRelationship retrieves the relationship of the targetAccount to the requestingAccount.
|
||||
GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error)
|
||||
|
||||
// Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out.
|
||||
Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error)
|
||||
|
||||
// FollowRequested returns true if sourceAccount has requested to follow target account, or an error if something goes wrong while finding out.
|
||||
FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// StatusParents get the parent statuses of a given status.
|
||||
//
|
||||
// If onlyDirect is true, only the immediate parent will be returned.
|
||||
StatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, error)
|
||||
|
||||
// StatusChildren gets the child statuses of a given status.
|
||||
//
|
||||
// If onlyDirect is true, only the immediate children will be returned.
|
||||
StatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// WhoBoostedStatus returns a slice of accounts who boosted 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.
|
||||
WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error)
|
||||
|
||||
// GetHomeTimelineForAccount returns a slice of statuses from accounts that are followed by the given account id.
|
||||
//
|
||||
// Statuses should be returned in descending order of when they were created (newest first).
|
||||
GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error)
|
||||
|
||||
// GetPublicTimelineForAccount fetches the account's PUBLIC timeline -- ie., posts and replies that are public.
|
||||
// It will use the given filters and try to return as many statuses as possible up to the limit.
|
||||
//
|
||||
// Statuses should be returned in descending order of when they were created (newest first).
|
||||
GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error)
|
||||
|
||||
// GetFavedTimelineForAccount fetches the account's FAVED timeline -- ie., posts and replies that the requesting account has faved.
|
||||
// It will use the given filters and try to return as many statuses as possible up to the limit.
|
||||
//
|
||||
// Note that unlike the other GetTimeline functions, the returned statuses will be arranged by their FAVE id, not the STATUS id.
|
||||
// In other words, they'll be returned in descending order of when they were faved by the requesting user, not when they were created.
|
||||
//
|
||||
// Also note the extra return values, which correspond to the nextMaxID and prevMinID for building Link headers.
|
||||
GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, error)
|
||||
|
||||
// GetNotificationsForAccount returns a list of notifications that pertain to the given accountID.
|
||||
GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error)
|
||||
|
||||
// GetUserCountForInstance returns the number of known accounts registered with the given domain.
|
||||
GetUserCountForInstance(domain string) (int, error)
|
||||
|
||||
// GetStatusCountForInstance returns the number of known statuses posted from the given domain.
|
||||
GetStatusCountForInstance(domain string) (int, error)
|
||||
|
||||
// GetDomainCountForInstance returns the number of known instances known that the given domain federates with.
|
||||
GetDomainCountForInstance(domain string) (int, error)
|
||||
|
||||
// GetAccountsForInstance returns a slice of accounts from the given instance, arranged by ID.
|
||||
GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, error)
|
||||
Account
|
||||
Admin
|
||||
Basic
|
||||
Instance
|
||||
Notification
|
||||
Relationship
|
||||
Status
|
||||
Timeline
|
||||
|
||||
/*
|
||||
USEFUL CONVERSION FUNCTIONS
|
||||
|
|
|
|||
17
internal/db/instance.go
Normal file
17
internal/db/instance.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package db
|
||||
|
||||
import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
||||
type Instance interface {
|
||||
// GetUserCountForInstance returns the number of known accounts registered with the given domain.
|
||||
GetUserCountForInstance(domain string) (int, error)
|
||||
|
||||
// GetStatusCountForInstance returns the number of known statuses posted from the given domain.
|
||||
GetStatusCountForInstance(domain string) (int, error)
|
||||
|
||||
// GetDomainCountForInstance returns the number of known instances known that the given domain federates with.
|
||||
GetDomainCountForInstance(domain string) (int, error)
|
||||
|
||||
// GetAccountsForInstance returns a slice of accounts from the given instance, arranged by ID.
|
||||
GetAccountsForInstance(domain string, maxID string, limit int) ([]*gtsmodel.Account, error)
|
||||
}
|
||||
8
internal/db/notification.go
Normal file
8
internal/db/notification.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package db
|
||||
|
||||
import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
||||
type Notification interface {
|
||||
// GetNotificationsForAccount returns a list of notifications that pertain to the given accountID.
|
||||
GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error)
|
||||
}
|
||||
236
internal/db/pg/account.go
Normal file
236
internal/db/pg/account.go
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
package pg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (ps *postgresService) GetAccountHeader(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 db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if acct.HeaderMediaAttachmentID == "" {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
|
||||
if err := ps.conn.Model(header).Where("id = ?", acct.HeaderMediaAttachmentID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetAccountAvatar(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 db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if acct.AvatarMediaAttachmentID == "" {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
|
||||
if err := ps.conn.Model(avatar).Where("id = ?", acct.AvatarMediaAttachmentID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetAccountLastStatus(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 db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (ps *postgresService) SetAccountHeaderOrAvatar(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) GetAccountByUserID(userID string, account *gtsmodel.Account) error {
|
||||
user := >smodel.User{
|
||||
ID: userID,
|
||||
}
|
||||
if err := ps.conn.Model(user).Where("id = ?", userID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := ps.conn.Model(account).Where("id = ?", user.AccountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetLocalAccountByUsername(username string, account *gtsmodel.Account) error {
|
||||
if err := ps.conn.Model(account).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetAccountFollowRequests(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 nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetAccountFollowing(accountID string, following *[]gtsmodel.Follow) error {
|
||||
if err := ps.conn.Model(following).Where("account_id = ?", accountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetAccountFollowers(accountID string, followers *[]gtsmodel.Follow, localOnly bool) error {
|
||||
|
||||
q := ps.conn.Model(followers)
|
||||
|
||||
if localOnly {
|
||||
// for local accounts let's get where domain is null OR where domain is an empty string, just to be safe
|
||||
whereGroup := func(q *pg.Query) (*pg.Query, error) {
|
||||
q = q.
|
||||
WhereOr("? IS NULL", pg.Ident("a.domain")).
|
||||
WhereOr("a.domain = ?", "")
|
||||
return q, nil
|
||||
}
|
||||
|
||||
q = q.ColumnExpr("follow.*").
|
||||
Join("JOIN accounts AS a ON follow.account_id = TEXT(a.id)").
|
||||
Where("follow.target_account_id = ?", accountID).
|
||||
WhereGroup(whereGroup)
|
||||
} else {
|
||||
q = q.Where("target_account_id = ?", accountID)
|
||||
}
|
||||
|
||||
if err := q.Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetAccountFaves(accountID string, faves *[]gtsmodel.StatusFave) error {
|
||||
if err := ps.conn.Model(faves).Where("account_id = ?", accountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetAccountStatusesCount(accountID string) (int, error) {
|
||||
count, err := ps.conn.Model(>smodel.Status{}).Where("account_id = ?", accountID).Count()
|
||||
if err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetAccountStatuses(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) {
|
||||
ps.log.Debugf("getting statuses for account %s", accountID)
|
||||
statuses := []*gtsmodel.Status{}
|
||||
|
||||
q := ps.conn.Model(&statuses).Order("id DESC")
|
||||
if accountID != "" {
|
||||
q = q.Where("account_id = ?", accountID)
|
||||
}
|
||||
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if excludeReplies {
|
||||
q = q.Where("? IS NULL", pg.Ident("in_reply_to_id"))
|
||||
}
|
||||
|
||||
if pinnedOnly {
|
||||
q = q.Where("pinned = ?", true)
|
||||
}
|
||||
|
||||
if mediaOnly {
|
||||
q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) {
|
||||
return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil
|
||||
})
|
||||
}
|
||||
|
||||
if maxID != "" {
|
||||
q = q.Where("id < ?", maxID)
|
||||
}
|
||||
|
||||
if err := q.Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return nil, db.ErrNoEntries{}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(statuses) == 0 {
|
||||
return nil, db.ErrNoEntries{}
|
||||
}
|
||||
|
||||
ps.log.Debugf("returning statuses for account %s", accountID)
|
||||
return statuses, nil
|
||||
}
|
||||
206
internal/db/pg/admin.go
Normal file
206
internal/db/pg/admin.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
package pg
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
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(>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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) IsEmailAvailable(email string) error {
|
||||
// parse the domain from the email
|
||||
m, err := mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing email address %s: %s", email, err)
|
||||
}
|
||||
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(>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 {
|
||||
// fail because we got an unexpected error
|
||||
return fmt.Errorf("db error: %s", err)
|
||||
}
|
||||
|
||||
// check if this email is associated with a user already
|
||||
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 {
|
||||
// fail because we got an unexpected error
|
||||
return fmt.Errorf("db error: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
ps.log.Errorf("error creating new rsa key: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if something went wrong while creating a user, we might already have an account, so check here first...
|
||||
a := >smodel.Account{}
|
||||
err = ps.conn.Model(a).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select()
|
||||
if err != nil {
|
||||
// there's been an actual error
|
||||
if err != pg.ErrNoRows {
|
||||
return nil, fmt.Errorf("db error checking existence of account: %s", err)
|
||||
}
|
||||
|
||||
// we just don't have an account yet create one
|
||||
newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host)
|
||||
newAccountID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a = >smodel.Account{
|
||||
ID: newAccountID,
|
||||
Username: username,
|
||||
DisplayName: username,
|
||||
Reason: reason,
|
||||
URL: newAccountURIs.UserURL,
|
||||
PrivateKey: key,
|
||||
PublicKey: &key.PublicKey,
|
||||
PublicKeyURI: newAccountURIs.PublicKeyURI,
|
||||
ActorType: gtsmodel.ActivityStreamsPerson,
|
||||
URI: newAccountURIs.UserURI,
|
||||
InboxURI: newAccountURIs.InboxURI,
|
||||
OutboxURI: newAccountURIs.OutboxURI,
|
||||
FollowersURI: newAccountURIs.FollowersURI,
|
||||
FollowingURI: newAccountURIs.FollowingURI,
|
||||
FeaturedCollectionURI: newAccountURIs.CollectionURI,
|
||||
}
|
||||
if _, err = ps.conn.Model(a).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error hashing password: %s", err)
|
||||
}
|
||||
|
||||
newUserID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := >smodel.User{
|
||||
ID: newUserID,
|
||||
AccountID: a.ID,
|
||||
EncryptedPassword: string(pw),
|
||||
SignUpIP: signUpIP.To4(),
|
||||
Locale: locale,
|
||||
UnconfirmedEmail: email,
|
||||
CreatedByApplicationID: appID,
|
||||
Approved: !requireApproval, // if we don't require moderator approval, just pre-approve the user
|
||||
}
|
||||
|
||||
if emailVerified {
|
||||
u.ConfirmedAt = time.Now()
|
||||
u.Email = email
|
||||
}
|
||||
|
||||
if admin {
|
||||
u.Admin = true
|
||||
u.Moderator = true
|
||||
}
|
||||
|
||||
if _, err = ps.conn.Model(u).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) CreateInstanceAccount() error {
|
||||
username := ps.config.Host
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
ps.log.Errorf("error creating new rsa key: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
aID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host)
|
||||
a := >smodel.Account{
|
||||
ID: aID,
|
||||
Username: ps.config.Host,
|
||||
DisplayName: username,
|
||||
URL: newAccountURIs.UserURL,
|
||||
PrivateKey: key,
|
||||
PublicKey: &key.PublicKey,
|
||||
PublicKeyURI: newAccountURIs.PublicKeyURI,
|
||||
ActorType: gtsmodel.ActivityStreamsPerson,
|
||||
URI: newAccountURIs.UserURI,
|
||||
InboxURI: newAccountURIs.InboxURI,
|
||||
OutboxURI: newAccountURIs.OutboxURI,
|
||||
FollowersURI: newAccountURIs.FollowersURI,
|
||||
FollowingURI: newAccountURIs.FollowingURI,
|
||||
FeaturedCollectionURI: newAccountURIs.CollectionURI,
|
||||
}
|
||||
inserted, err := ps.conn.Model(a).Where("username = ?", username).SelectOrInsert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inserted {
|
||||
ps.log.Infof("created instance account %s with id %s", username, a.ID)
|
||||
} else {
|
||||
ps.log.Infof("instance account %s already exists with id %s", username, a.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) CreateInstanceInstance() error {
|
||||
iID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := >smodel.Instance{
|
||||
ID: iID,
|
||||
Domain: ps.config.Host,
|
||||
Title: ps.config.Host,
|
||||
URI: fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host),
|
||||
}
|
||||
inserted, err := ps.conn.Model(i).Where("domain = ?", ps.config.Host).SelectOrInsert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inserted {
|
||||
ps.log.Infof("created instance instance %s with id %s", ps.config.Host, i.ID)
|
||||
} else {
|
||||
ps.log.Infof("instance instance %s already exists with id %s", ps.config.Host, i.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,30 +1,21 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package pg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
)
|
||||
|
||||
func (ps *postgresService) Put(i interface{}) error {
|
||||
_, err := ps.conn.Model(i).Insert(i)
|
||||
if err != nil && strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
|
||||
return db.ErrAlreadyExists{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetByID(id string, i interface{}) error {
|
||||
if err := ps.conn.Model(i).Where("id = ?", id).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
|
|
@ -73,3 +64,34 @@ func (ps *postgresService) GetAll(i interface{}) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) DeleteByID(id string, i interface{}) error {
|
||||
if _, err := ps.conn.Model(i).Where("id = ?", id).Delete(); err != nil {
|
||||
// if there are no rows *anyway* then that's fine
|
||||
// just return err if there's an actual error
|
||||
if err != pg.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) DeleteWhere(where []db.Where, i interface{}) error {
|
||||
if len(where) == 0 {
|
||||
return errors.New("no queries provided")
|
||||
}
|
||||
|
||||
q := ps.conn.Model(i)
|
||||
for _, w := range where {
|
||||
q = q.Where("? = ?", pg.Safe(w.Key), w.Value)
|
||||
}
|
||||
|
||||
if _, err := q.Delete(); err != nil {
|
||||
// if there are no rows *anyway* then that's fine
|
||||
// just return err if there's an actual error
|
||||
if err != pg.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (ps *postgresService) GetBlocksForAccount(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error) {
|
||||
func (ps *postgresService) GetAccountBlocks(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error) {
|
||||
blocks := []*gtsmodel.Block{}
|
||||
|
||||
fq := ps.conn.Model(&blocks).
|
||||
|
|
|
|||
|
|
@ -1,57 +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 pg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
)
|
||||
|
||||
func (ps *postgresService) DeleteByID(id string, i interface{}) error {
|
||||
if _, err := ps.conn.Model(i).Where("id = ?", id).Delete(); err != nil {
|
||||
// if there are no rows *anyway* then that's fine
|
||||
// just return err if there's an actual error
|
||||
if err != pg.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) DeleteWhere(where []db.Where, i interface{}) error {
|
||||
if len(where) == 0 {
|
||||
return errors.New("no queries provided")
|
||||
}
|
||||
|
||||
q := ps.conn.Model(i)
|
||||
for _, w := range where {
|
||||
q = q.Where("? = ?", pg.Safe(w.Key), w.Value)
|
||||
}
|
||||
|
||||
if _, err := q.Delete(); err != nil {
|
||||
// if there are no rows *anyway* then that's fine
|
||||
// just return err if there's an actual error
|
||||
if err != pg.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -20,15 +20,11 @@ package pg
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -41,8 +37,6 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// postgresService satisfies the DB interface
|
||||
|
|
@ -254,641 +248,6 @@ func (ps *postgresService) CreateSchema(ctx context.Context) error {
|
|||
HANDY SHORTCUTS
|
||||
*/
|
||||
|
||||
func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) {
|
||||
// make sure the original follow request exists
|
||||
fr := >smodel.FollowRequest{}
|
||||
if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil {
|
||||
if err == pg.ErrMultiRows {
|
||||
return nil, db.ErrNoEntries{}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create a new follow to 'replace' the request with
|
||||
follow := >smodel.Follow{
|
||||
ID: fr.ID,
|
||||
AccountID: originAccountID,
|
||||
TargetAccountID: targetAccountID,
|
||||
URI: fr.URI,
|
||||
}
|
||||
|
||||
// if the follow already exists, just update the URI -- we don't need to do anything else
|
||||
if _, err := ps.conn.Model(follow).OnConflict("ON CONSTRAINT follows_account_id_target_account_id_key DO UPDATE set uri = ?", follow.URI).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// now remove the follow request
|
||||
if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return follow, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) CreateInstanceAccount() error {
|
||||
username := ps.config.Host
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
ps.log.Errorf("error creating new rsa key: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
aID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host)
|
||||
a := >smodel.Account{
|
||||
ID: aID,
|
||||
Username: ps.config.Host,
|
||||
DisplayName: username,
|
||||
URL: newAccountURIs.UserURL,
|
||||
PrivateKey: key,
|
||||
PublicKey: &key.PublicKey,
|
||||
PublicKeyURI: newAccountURIs.PublicKeyURI,
|
||||
ActorType: gtsmodel.ActivityStreamsPerson,
|
||||
URI: newAccountURIs.UserURI,
|
||||
InboxURI: newAccountURIs.InboxURI,
|
||||
OutboxURI: newAccountURIs.OutboxURI,
|
||||
FollowersURI: newAccountURIs.FollowersURI,
|
||||
FollowingURI: newAccountURIs.FollowingURI,
|
||||
FeaturedCollectionURI: newAccountURIs.CollectionURI,
|
||||
}
|
||||
inserted, err := ps.conn.Model(a).Where("username = ?", username).SelectOrInsert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inserted {
|
||||
ps.log.Infof("created instance account %s with id %s", username, a.ID)
|
||||
} else {
|
||||
ps.log.Infof("instance account %s already exists with id %s", username, a.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) CreateInstanceInstance() error {
|
||||
iID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := >smodel.Instance{
|
||||
ID: iID,
|
||||
Domain: ps.config.Host,
|
||||
Title: ps.config.Host,
|
||||
URI: fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host),
|
||||
}
|
||||
inserted, err := ps.conn.Model(i).Where("domain = ?", ps.config.Host).SelectOrInsert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inserted {
|
||||
ps.log.Infof("created instance instance %s with id %s", ps.config.Host, i.ID)
|
||||
} else {
|
||||
ps.log.Infof("instance instance %s already exists with id %s", ps.config.Host, i.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 {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := ps.conn.Model(account).Where("id = ?", user.AccountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetLocalAccountByUsername(username string, account *gtsmodel.Account) error {
|
||||
if err := ps.conn.Model(account).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow, localOnly bool) error {
|
||||
|
||||
q := ps.conn.Model(followers)
|
||||
|
||||
if localOnly {
|
||||
// for local accounts let's get where domain is null OR where domain is an empty string, just to be safe
|
||||
whereGroup := func(q *pg.Query) (*pg.Query, error) {
|
||||
q = q.
|
||||
WhereOr("? IS NULL", pg.Ident("a.domain")).
|
||||
WhereOr("a.domain = ?", "")
|
||||
return q, nil
|
||||
}
|
||||
|
||||
q = q.ColumnExpr("follow.*").
|
||||
Join("JOIN accounts AS a ON follow.account_id = TEXT(a.id)").
|
||||
Where("follow.target_account_id = ?", accountID).
|
||||
WhereGroup(whereGroup)
|
||||
} else {
|
||||
q = q.Where("target_account_id = ?", accountID)
|
||||
}
|
||||
|
||||
if err := q.Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetFavesByAccountID(accountID string, faves *[]gtsmodel.StatusFave) error {
|
||||
if err := ps.conn.Model(faves).Where("account_id = ?", accountID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) CountStatusesByAccountID(accountID string) (int, error) {
|
||||
count, err := ps.conn.Model(>smodel.Status{}).Where("account_id = ?", accountID).Count()
|
||||
if err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) {
|
||||
ps.log.Debugf("getting statuses for account %s", accountID)
|
||||
statuses := []*gtsmodel.Status{}
|
||||
|
||||
q := ps.conn.Model(&statuses).Order("id DESC")
|
||||
if accountID != "" {
|
||||
q = q.Where("account_id = ?", accountID)
|
||||
}
|
||||
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if excludeReplies {
|
||||
q = q.Where("? IS NULL", pg.Ident("in_reply_to_id"))
|
||||
}
|
||||
|
||||
if pinnedOnly {
|
||||
q = q.Where("pinned = ?", true)
|
||||
}
|
||||
|
||||
if mediaOnly {
|
||||
q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) {
|
||||
return q.Where("? IS NOT NULL", pg.Ident("attachments")).Where("attachments != '{}'"), nil
|
||||
})
|
||||
}
|
||||
|
||||
if maxID != "" {
|
||||
q = q.Where("id < ?", maxID)
|
||||
}
|
||||
|
||||
if err := q.Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return nil, db.ErrNoEntries{}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(statuses) == 0 {
|
||||
return nil, db.ErrNoEntries{}
|
||||
}
|
||||
|
||||
ps.log.Debugf("returning statuses for account %s", accountID)
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
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 db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
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(>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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) IsEmailAvailable(email string) error {
|
||||
// parse the domain from the email
|
||||
m, err := mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing email address %s: %s", email, err)
|
||||
}
|
||||
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(>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 {
|
||||
// fail because we got an unexpected error
|
||||
return fmt.Errorf("db error: %s", err)
|
||||
}
|
||||
|
||||
// check if this email is associated with a user already
|
||||
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 {
|
||||
// fail because we got an unexpected error
|
||||
return fmt.Errorf("db error: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) NewSignup(username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, admin bool) (*gtsmodel.User, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
ps.log.Errorf("error creating new rsa key: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if something went wrong while creating a user, we might already have an account, so check here first...
|
||||
a := >smodel.Account{}
|
||||
err = ps.conn.Model(a).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select()
|
||||
if err != nil {
|
||||
// there's been an actual error
|
||||
if err != pg.ErrNoRows {
|
||||
return nil, fmt.Errorf("db error checking existence of account: %s", err)
|
||||
}
|
||||
|
||||
// we just don't have an account yet create one
|
||||
newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host)
|
||||
newAccountID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a = >smodel.Account{
|
||||
ID: newAccountID,
|
||||
Username: username,
|
||||
DisplayName: username,
|
||||
Reason: reason,
|
||||
URL: newAccountURIs.UserURL,
|
||||
PrivateKey: key,
|
||||
PublicKey: &key.PublicKey,
|
||||
PublicKeyURI: newAccountURIs.PublicKeyURI,
|
||||
ActorType: gtsmodel.ActivityStreamsPerson,
|
||||
URI: newAccountURIs.UserURI,
|
||||
InboxURI: newAccountURIs.InboxURI,
|
||||
OutboxURI: newAccountURIs.OutboxURI,
|
||||
FollowersURI: newAccountURIs.FollowersURI,
|
||||
FollowingURI: newAccountURIs.FollowingURI,
|
||||
FeaturedCollectionURI: newAccountURIs.CollectionURI,
|
||||
}
|
||||
if _, err = ps.conn.Model(a).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error hashing password: %s", err)
|
||||
}
|
||||
|
||||
newUserID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := >smodel.User{
|
||||
ID: newUserID,
|
||||
AccountID: a.ID,
|
||||
EncryptedPassword: string(pw),
|
||||
SignUpIP: signUpIP.To4(),
|
||||
Locale: locale,
|
||||
UnconfirmedEmail: email,
|
||||
CreatedByApplicationID: appID,
|
||||
Approved: !requireApproval, // if we don't require moderator approval, just pre-approve the user
|
||||
}
|
||||
|
||||
if emailVerified {
|
||||
u.ConfirmedAt = time.Now()
|
||||
u.Email = email
|
||||
}
|
||||
|
||||
if admin {
|
||||
u.Admin = true
|
||||
u.Moderator = true
|
||||
}
|
||||
|
||||
if _, err = ps.conn.Model(u).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
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 *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 db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if acct.HeaderMediaAttachmentID == "" {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
|
||||
if err := ps.conn.Model(header).Where("id = ?", acct.HeaderMediaAttachmentID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return 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 db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if acct.AvatarMediaAttachmentID == "" {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
|
||||
if err := ps.conn.Model(avatar).Where("id = ?", acct.AvatarMediaAttachmentID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) {
|
||||
// TODO: check domain blocks as well
|
||||
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
|
||||
}
|
||||
return blocked, err
|
||||
}
|
||||
blocked = true
|
||||
return blocked, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) {
|
||||
r := >smodel.Relationship{
|
||||
ID: targetAccount,
|
||||
}
|
||||
|
||||
// check if the requesting account follows the target account
|
||||
follow := >smodel.Follow{}
|
||||
if err := ps.conn.Model(follow).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Select(); err != nil {
|
||||
if err != pg.ErrNoRows {
|
||||
// a proper error
|
||||
return nil, fmt.Errorf("getrelationship: error checking follow existence: %s", err)
|
||||
}
|
||||
// no follow exists so these are all false
|
||||
r.Following = false
|
||||
r.ShowingReblogs = false
|
||||
r.Notifying = false
|
||||
} else {
|
||||
// follow exists so we can fill these fields out...
|
||||
r.Following = true
|
||||
r.ShowingReblogs = follow.ShowReblogs
|
||||
r.Notifying = follow.Notify
|
||||
}
|
||||
|
||||
// check if the target account follows the requesting account
|
||||
followedBy, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking followed_by existence: %s", err)
|
||||
}
|
||||
r.FollowedBy = followedBy
|
||||
|
||||
// check if the requesting account blocks the target account
|
||||
blocking, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking blocking existence: %s", err)
|
||||
}
|
||||
r.Blocking = blocking
|
||||
|
||||
// check if the target account blocks the requesting account
|
||||
blockedBy, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err)
|
||||
}
|
||||
r.BlockedBy = blockedBy
|
||||
|
||||
// check if there's a pending following request from requesting account to target account
|
||||
requested, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err)
|
||||
}
|
||||
r.Requested = requested
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) {
|
||||
if sourceAccount == nil || targetAccount == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) {
|
||||
if sourceAccount == nil || targetAccount == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) {
|
||||
if account1 == nil || account2 == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return f1 && f2, 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) 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
|
||||
}
|
||||
|
||||
func (ps *postgresService) WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) {
|
||||
accounts := []*gtsmodel.Account{}
|
||||
|
||||
boosts := []*gtsmodel.Status{}
|
||||
if err := ps.conn.Model(&boosts).Where("boost_of_id = ?", status.ID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return accounts, nil // no rows just means nobody has boosted this status, so that's fine
|
||||
}
|
||||
return nil, err // an actual error has occurred
|
||||
}
|
||||
|
||||
for _, f := range boosts {
|
||||
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
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error) {
|
||||
notifications := []*gtsmodel.Notification{}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +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 pg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
)
|
||||
|
||||
func (ps *postgresService) Put(i interface{}) error {
|
||||
_, err := ps.conn.Model(i).Insert(i)
|
||||
if err != nil && strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
|
||||
return db.ErrAlreadyExists{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
153
internal/db/pg/relationship.go
Normal file
153
internal/db/pg/relationship.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package pg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) {
|
||||
// TODO: check domain blocks as well
|
||||
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
|
||||
}
|
||||
return blocked, err
|
||||
}
|
||||
blocked = true
|
||||
return blocked, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) {
|
||||
r := >smodel.Relationship{
|
||||
ID: targetAccount,
|
||||
}
|
||||
|
||||
// check if the requesting account follows the target account
|
||||
follow := >smodel.Follow{}
|
||||
if err := ps.conn.Model(follow).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Select(); err != nil {
|
||||
if err != pg.ErrNoRows {
|
||||
// a proper error
|
||||
return nil, fmt.Errorf("getrelationship: error checking follow existence: %s", err)
|
||||
}
|
||||
// no follow exists so these are all false
|
||||
r.Following = false
|
||||
r.ShowingReblogs = false
|
||||
r.Notifying = false
|
||||
} else {
|
||||
// follow exists so we can fill these fields out...
|
||||
r.Following = true
|
||||
r.ShowingReblogs = follow.ShowReblogs
|
||||
r.Notifying = follow.Notify
|
||||
}
|
||||
|
||||
// check if the target account follows the requesting account
|
||||
followedBy, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking followed_by existence: %s", err)
|
||||
}
|
||||
r.FollowedBy = followedBy
|
||||
|
||||
// check if the requesting account blocks the target account
|
||||
blocking, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking blocking existence: %s", err)
|
||||
}
|
||||
r.Blocking = blocking
|
||||
|
||||
// check if the target account blocks the requesting account
|
||||
blockedBy, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err)
|
||||
}
|
||||
r.BlockedBy = blockedBy
|
||||
|
||||
// check if there's a pending following request from requesting account to target account
|
||||
requested, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err)
|
||||
}
|
||||
r.Requested = requested
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) {
|
||||
if sourceAccount == nil || targetAccount == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) {
|
||||
if sourceAccount == nil || targetAccount == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) {
|
||||
if account1 == nil || account2 == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return f1 && f2, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) {
|
||||
// make sure the original follow request exists
|
||||
fr := >smodel.FollowRequest{}
|
||||
if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil {
|
||||
if err == pg.ErrMultiRows {
|
||||
return nil, db.ErrNoEntries{}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create a new follow to 'replace' the request with
|
||||
follow := >smodel.Follow{
|
||||
ID: fr.ID,
|
||||
AccountID: originAccountID,
|
||||
TargetAccountID: targetAccountID,
|
||||
URI: fr.URI,
|
||||
}
|
||||
|
||||
// if the follow already exists, just update the URI -- we don't need to do anything else
|
||||
if _, err := ps.conn.Model(follow).OnConflict("ON CONSTRAINT follows_account_id_target_account_id_key DO UPDATE set uri = ?", follow.URI).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// now remove the follow request
|
||||
if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return follow, nil
|
||||
}
|
||||
181
internal/db/pg/status.go
Normal file
181
internal/db/pg/status.go
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
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 pg
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"errors"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (ps *postgresService) StatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, error) {
|
||||
parents := []*gtsmodel.Status{}
|
||||
ps.statusParent(status, &parents, onlyDirect)
|
||||
|
||||
return parents, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) statusParent(status *gtsmodel.Status, foundStatuses *[]*gtsmodel.Status, onlyDirect bool) {
|
||||
if status.InReplyToID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
parentStatus := >smodel.Status{}
|
||||
if err := ps.conn.Model(parentStatus).Where("id = ?", status.InReplyToID).Select(); err == nil {
|
||||
*foundStatuses = append(*foundStatuses, parentStatus)
|
||||
}
|
||||
|
||||
if onlyDirect {
|
||||
return
|
||||
}
|
||||
ps.statusParent(parentStatus, foundStatuses, false)
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, error) {
|
||||
foundStatuses := &list.List{}
|
||||
foundStatuses.PushFront(status)
|
||||
ps.statusChildren(status, foundStatuses, onlyDirect, minID)
|
||||
|
||||
children := []*gtsmodel.Status{}
|
||||
for e := foundStatuses.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status"))
|
||||
}
|
||||
|
||||
// only append children, not the overall parent status
|
||||
if entry.ID != status.ID {
|
||||
children = append(children, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return children, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) statusChildren(status *gtsmodel.Status, foundStatuses *list.List, onlyDirect bool, minID string) {
|
||||
immediateChildren := []*gtsmodel.Status{}
|
||||
|
||||
q := ps.conn.Model(&immediateChildren).Where("in_reply_to_id = ?", status.ID)
|
||||
if minID != "" {
|
||||
q = q.Where("status.id > ?", minID)
|
||||
}
|
||||
|
||||
if err := q.Select(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, child := range immediateChildren {
|
||||
insertLoop:
|
||||
for e := foundStatuses.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status"))
|
||||
}
|
||||
|
||||
if child.InReplyToAccountID != "" && entry.ID == child.InReplyToID {
|
||||
foundStatuses.InsertAfter(child, e)
|
||||
break insertLoop
|
||||
}
|
||||
}
|
||||
|
||||
// only do one loop if we only want direct children
|
||||
if onlyDirect {
|
||||
return
|
||||
}
|
||||
ps.statusChildren(child, foundStatuses, false, minID)
|
||||
}
|
||||
}
|
||||
|
||||
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) 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
|
||||
}
|
||||
|
||||
func (ps *postgresService) WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) {
|
||||
accounts := []*gtsmodel.Account{}
|
||||
|
||||
boosts := []*gtsmodel.Status{}
|
||||
if err := ps.conn.Model(&boosts).Where("boost_of_id = ?", status.ID).Select(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return accounts, nil // no rows just means nobody has boosted this status, so that's fine
|
||||
}
|
||||
return nil, err // an actual error has occurred
|
||||
}
|
||||
|
||||
for _, f := range boosts {
|
||||
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
|
||||
}
|
||||
|
|
@ -1,104 +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 pg
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"errors"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (ps *postgresService) StatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, error) {
|
||||
parents := []*gtsmodel.Status{}
|
||||
ps.statusParent(status, &parents, onlyDirect)
|
||||
|
||||
return parents, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) statusParent(status *gtsmodel.Status, foundStatuses *[]*gtsmodel.Status, onlyDirect bool) {
|
||||
if status.InReplyToID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
parentStatus := >smodel.Status{}
|
||||
if err := ps.conn.Model(parentStatus).Where("id = ?", status.InReplyToID).Select(); err == nil {
|
||||
*foundStatuses = append(*foundStatuses, parentStatus)
|
||||
}
|
||||
|
||||
if onlyDirect {
|
||||
return
|
||||
}
|
||||
ps.statusParent(parentStatus, foundStatuses, false)
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, error) {
|
||||
foundStatuses := &list.List{}
|
||||
foundStatuses.PushFront(status)
|
||||
ps.statusChildren(status, foundStatuses, onlyDirect, minID)
|
||||
|
||||
children := []*gtsmodel.Status{}
|
||||
for e := foundStatuses.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status"))
|
||||
}
|
||||
|
||||
// only append children, not the overall parent status
|
||||
if entry.ID != status.ID {
|
||||
children = append(children, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return children, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) statusChildren(status *gtsmodel.Status, foundStatuses *list.List, onlyDirect bool, minID string) {
|
||||
immediateChildren := []*gtsmodel.Status{}
|
||||
|
||||
q := ps.conn.Model(&immediateChildren).Where("in_reply_to_id = ?", status.ID)
|
||||
if minID != "" {
|
||||
q = q.Where("status.id > ?", minID)
|
||||
}
|
||||
|
||||
if err := q.Select(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, child := range immediateChildren {
|
||||
insertLoop:
|
||||
for e := foundStatuses.Front(); e != nil; e = e.Next() {
|
||||
entry, ok := e.Value.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
panic(errors.New("entry in foundStatuses was not a *gtsmodel.Status"))
|
||||
}
|
||||
|
||||
if child.InReplyToAccountID != "" && entry.ID == child.InReplyToID {
|
||||
foundStatuses.InsertAfter(child, e)
|
||||
break insertLoop
|
||||
}
|
||||
}
|
||||
|
||||
// only do one loop if we only want direct children
|
||||
if onlyDirect {
|
||||
return
|
||||
}
|
||||
ps.statusChildren(child, foundStatuses, false, minID)
|
||||
}
|
||||
}
|
||||
27
internal/db/relationship.go
Normal file
27
internal/db/relationship.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package db
|
||||
|
||||
import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
||||
type Relationship interface {
|
||||
// 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)
|
||||
|
||||
// GetRelationship retrieves the relationship of the targetAccount to the requestingAccount.
|
||||
GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error)
|
||||
|
||||
// Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out.
|
||||
Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error)
|
||||
|
||||
// FollowRequested returns true if sourceAccount has requested to follow target account, or an error if something goes wrong while finding out.
|
||||
FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, 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)
|
||||
|
||||
// AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table.
|
||||
// In other words, it should create the follow, and delete the existing follow request.
|
||||
//
|
||||
// It will return the newly created follow for further processing.
|
||||
AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error)
|
||||
}
|
||||
44
internal/db/status.go
Normal file
44
internal/db/status.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package db
|
||||
|
||||
import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
||||
type Status interface {
|
||||
// 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)
|
||||
|
||||
// StatusParents get the parent statuses of a given status.
|
||||
//
|
||||
// If onlyDirect is true, only the immediate parent will be returned.
|
||||
StatusParents(status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, error)
|
||||
|
||||
// StatusChildren gets the child statuses of a given status.
|
||||
//
|
||||
// If onlyDirect is true, only the immediate children will be returned.
|
||||
StatusChildren(status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// WhoBoostedStatus returns a slice of accounts who boosted 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.
|
||||
WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error)
|
||||
}
|
||||
25
internal/db/timeline.go
Normal file
25
internal/db/timeline.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package db
|
||||
|
||||
import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
||||
type Timeline interface {
|
||||
// GetHomeTimelineForAccount returns a slice of statuses from accounts that are followed by the given account id.
|
||||
//
|
||||
// Statuses should be returned in descending order of when they were created (newest first).
|
||||
GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error)
|
||||
|
||||
// GetPublicTimelineForAccount fetches the account's PUBLIC timeline -- ie., posts and replies that are public.
|
||||
// It will use the given filters and try to return as many statuses as possible up to the limit.
|
||||
//
|
||||
// Statuses should be returned in descending order of when they were created (newest first).
|
||||
GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error)
|
||||
|
||||
// GetFavedTimelineForAccount fetches the account's FAVED timeline -- ie., posts and replies that the requesting account has faved.
|
||||
// It will use the given filters and try to return as many statuses as possible up to the limit.
|
||||
//
|
||||
// Note that unlike the other GetTimeline functions, the returned statuses will be arranged by their FAVE id, not the STATUS id.
|
||||
// In other words, they'll be returned in descending order of when they were faved by the requesting user, not when they were created.
|
||||
//
|
||||
// Also note the extra return values, which correspond to the nextMaxID and prevMinID for building Link headers.
|
||||
GetFavedTimelineForAccount(accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, error)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue