2021-03-02 17:26:30 +00:00
/ *
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/>.
* /
2021-05-15 09:58:11 +00:00
package pg
2021-03-02 17:26:30 +00:00
import (
"context"
2021-04-01 18:46:45 +00:00
"crypto/rand"
"crypto/rsa"
2021-03-02 17:26:30 +00:00
"errors"
"fmt"
2021-04-01 18:46:45 +00:00
"net"
"net/mail"
2021-03-04 13:38:18 +00:00
"regexp"
2021-03-03 17:12:02 +00:00
"strings"
2021-03-02 21:52:31 +00:00
"time"
2021-03-02 17:26:30 +00:00
2021-03-05 17:31:12 +00:00
"github.com/go-pg/pg/extra/pgdebug"
"github.com/go-pg/pg/v10"
"github.com/go-pg/pg/v10/orm"
2021-04-19 17:42:19 +00:00
"github.com/google/uuid"
2021-03-02 21:52:31 +00:00
"github.com/sirupsen/logrus"
2021-04-01 18:46:45 +00:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2021-05-15 09:58:11 +00:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2021-05-08 12:25:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2021-04-01 18:46:45 +00:00
"github.com/superseriousbusiness/gotosocial/internal/util"
"golang.org/x/crypto/bcrypt"
2021-03-02 17:26:30 +00:00
)
2021-03-22 21:26:54 +00:00
// postgresService satisfies the DB interface
2021-03-02 17:26:30 +00:00
type postgresService struct {
2021-05-21 13:48:26 +00:00
config * config . Config
conn * pg . DB
log * logrus . Logger
cancel context . CancelFunc
// federationDB pub.Database
2021-03-02 17:26:30 +00:00
}
2021-05-08 12:25:55 +00:00
// NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface.
2021-03-02 17:26:30 +00:00
// Under the hood, it uses https://github.com/go-pg/pg to create and maintain a database connection.
2021-05-15 09:58:11 +00:00
func NewPostgresService ( ctx context . Context , c * config . Config , log * logrus . Logger ) ( db . DB , error ) {
2021-03-04 13:38:18 +00:00
opts , err := derivePGOptions ( c )
2021-03-02 17:26:30 +00:00
if err != nil {
return nil , fmt . Errorf ( "could not create postgres service: %s" , err )
}
2021-03-05 17:31:12 +00:00
log . Debugf ( "using pg options: %+v" , opts )
2021-03-02 21:52:31 +00:00
// create a connection
pgCtx , cancel := context . WithCancel ( ctx )
conn := pg . Connect ( opts ) . WithContext ( pgCtx )
2021-03-05 17:31:12 +00:00
// this will break the logfmt format we normally log in,
// since we can't choose where pg outputs to and it defaults to
// stdout. So use this option with care!
2021-05-08 12:25:55 +00:00
if log . GetLevel ( ) >= logrus . TraceLevel {
2021-03-05 17:31:12 +00:00
conn . AddQueryHook ( pgdebug . DebugHook {
// Print all queries.
Verbose : true ,
} )
}
2021-04-19 17:42:19 +00:00
// actually *begin* the connection so that we can tell if the db is there and listening
2021-03-05 17:31:12 +00:00
if err := conn . Ping ( ctx ) ; err != nil {
2021-03-02 21:52:31 +00:00
cancel ( )
return nil , fmt . Errorf ( "db connection error: %s" , err )
}
2021-03-05 17:31:12 +00:00
// print out discovered postgres version
var version string
if _ , err = conn . QueryOneContext ( ctx , pg . Scan ( & version ) , "SELECT version()" ) ; err != nil {
2021-03-02 21:52:31 +00:00
cancel ( )
return nil , fmt . Errorf ( "db connection error: %s" , err )
}
2021-03-05 17:31:12 +00:00
log . Infof ( "connected to postgres version: %s" , version )
2021-03-02 21:52:31 +00:00
2021-04-01 18:46:45 +00:00
ps := & postgresService {
config : c ,
conn : conn ,
log : log ,
cancel : cancel ,
}
2021-03-02 17:26:30 +00:00
2021-04-01 18:46:45 +00:00
// we can confidently return this useable postgres service now
return ps , nil
2021-03-22 21:26:54 +00:00
}
2021-03-02 17:26:30 +00:00
/ *
HANDY STUFF
* /
// derivePGOptions takes an application config and returns either a ready-to-use *pg.Options
// with sensible defaults, or an error if it's not satisfied by the provided config.
2021-03-04 13:38:18 +00:00
func derivePGOptions ( c * config . Config ) ( * pg . Options , error ) {
2021-05-15 09:58:11 +00:00
if strings . ToUpper ( c . DBConfig . Type ) != db . DBTypePostgres {
return nil , fmt . Errorf ( "expected db type of %s but got %s" , db . DBTypePostgres , c . DBConfig . Type )
2021-03-02 17:26:30 +00:00
}
2021-03-04 11:07:24 +00:00
// validate port
2021-03-04 13:38:18 +00:00
if c . DBConfig . Port == 0 {
2021-03-04 11:07:24 +00:00
return nil , errors . New ( "no port set" )
2021-03-02 17:26:30 +00:00
}
// validate address
2021-03-04 13:38:18 +00:00
if c . DBConfig . Address == "" {
2021-03-04 11:07:24 +00:00
return nil , errors . New ( "no address set" )
2021-03-02 21:52:31 +00:00
}
2021-03-04 13:38:18 +00:00
ipv4Regex := regexp . MustCompile ( ` ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.) { 3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$ ` )
hostnameRegex := regexp . MustCompile ( ` ^(?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z] { 2,}$ ` )
if ! hostnameRegex . MatchString ( c . DBConfig . Address ) && ! ipv4Regex . MatchString ( c . DBConfig . Address ) && c . DBConfig . Address != "localhost" {
return nil , fmt . Errorf ( "address %s was neither an ipv4 address nor a valid hostname" , c . DBConfig . Address )
2021-03-02 21:52:31 +00:00
}
// validate username
2021-03-04 13:38:18 +00:00
if c . DBConfig . User == "" {
2021-03-04 11:07:24 +00:00
return nil , errors . New ( "no user set" )
2021-03-02 17:26:30 +00:00
}
2021-03-02 21:52:31 +00:00
// validate that there's a password
2021-03-04 13:38:18 +00:00
if c . DBConfig . Password == "" {
2021-03-02 21:52:31 +00:00
return nil , errors . New ( "no password set" )
}
// validate database
2021-03-04 13:38:18 +00:00
if c . DBConfig . Database == "" {
2021-03-04 11:07:24 +00:00
return nil , errors . New ( "no database set" )
2021-03-02 17:26:30 +00:00
}
2021-03-02 21:52:31 +00:00
// We can rely on the pg library we're using to set
// sensible defaults for everything we don't set here.
2021-03-02 17:26:30 +00:00
options := & pg . Options {
2021-03-04 13:38:18 +00:00
Addr : fmt . Sprintf ( "%s:%d" , c . DBConfig . Address , c . DBConfig . Port ) ,
User : c . DBConfig . User ,
Password : c . DBConfig . Password ,
Database : c . DBConfig . Database ,
ApplicationName : c . ApplicationName ,
2021-03-02 17:26:30 +00:00
}
return options , nil
}
2021-04-01 18:46:45 +00:00
/ *
BASIC DB FUNCTIONALITY
* /
func ( ps * postgresService ) CreateTable ( i interface { } ) error {
return ps . conn . Model ( i ) . CreateTable ( & orm . CreateTableOptions {
IfNotExists : true ,
} )
}
func ( ps * postgresService ) DropTable ( i interface { } ) error {
return ps . conn . Model ( i ) . DropTable ( & orm . DropTableOptions {
IfExists : true ,
} )
}
2021-03-02 21:52:31 +00:00
func ( ps * postgresService ) Stop ( ctx context . Context ) error {
ps . log . Info ( "closing db connection" )
if err := ps . conn . Close ( ) ; err != nil {
// only cancel if there's a problem closing the db
ps . cancel ( )
return err
}
return nil
2021-03-02 17:26:30 +00:00
}
2021-03-05 17:31:12 +00:00
2021-04-01 18:46:45 +00:00
func ( ps * postgresService ) IsHealthy ( ctx context . Context ) error {
return ps . conn . Ping ( ctx )
}
2021-03-05 17:31:12 +00:00
func ( ps * postgresService ) CreateSchema ( ctx context . Context ) error {
models := [ ] interface { } {
2021-04-19 17:42:19 +00:00
( * gtsmodel . Account ) ( nil ) ,
( * gtsmodel . Status ) ( nil ) ,
( * gtsmodel . User ) ( nil ) ,
2021-03-05 17:31:12 +00:00
}
ps . log . Info ( "creating db schema" )
for _ , model := range models {
err := ps . conn . Model ( model ) . CreateTable ( & orm . CreateTableOptions {
IfNotExists : true ,
} )
if err != nil {
return err
}
}
ps . log . Info ( "db schema created" )
return nil
}
2021-03-22 21:26:54 +00:00
func ( ps * postgresService ) GetByID ( id string , i interface { } ) error {
2021-04-01 18:46:45 +00:00
if err := ps . conn . Model ( i ) . Where ( "id = ?" , id ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-01 18:46:45 +00:00
}
return err
}
return nil
2021-03-22 21:26:54 +00:00
}
2021-05-21 13:48:26 +00:00
func ( ps * postgresService ) GetWhere ( 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 {
2021-05-29 17:39:43 +00:00
if w . CaseInsensitive {
q = q . Where ( "LOWER(?) = LOWER(?)" , pg . Safe ( w . Key ) , w . Value )
} else {
q = q . Where ( "? = ?" , pg . Safe ( w . Key ) , w . Value )
}
2021-05-21 13:48:26 +00:00
}
if err := q . Select ( ) ; err != nil {
2021-04-01 18:46:45 +00:00
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-01 18:46:45 +00:00
}
return err
}
return nil
2021-03-22 21:26:54 +00:00
}
2021-04-19 17:42:19 +00:00
// func (ps *postgresService) GetWhereMany(i interface{}, where ...model.Where) error {
// return nil
// }
2021-03-22 21:26:54 +00:00
func ( ps * postgresService ) GetAll ( i interface { } ) error {
2021-04-01 18:46:45 +00:00
if err := ps . conn . Model ( i ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-01 18:46:45 +00:00
}
return err
}
return nil
2021-03-22 21:26:54 +00:00
}
func ( ps * postgresService ) Put ( i interface { } ) error {
_ , err := ps . conn . Model ( i ) . Insert ( i )
2021-05-21 13:48:26 +00:00
if err != nil && strings . Contains ( err . Error ( ) , "duplicate key value violates unique constraint" ) {
return db . ErrAlreadyExists { }
}
2021-03-22 21:26:54 +00:00
return err
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) Upsert ( i interface { } , conflictColumn string ) error {
if _ , err := ps . conn . Model ( i ) . OnConflict ( fmt . Sprintf ( "(%s) DO UPDATE" , conflictColumn ) ) . Insert ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-19 17:42:19 +00:00
}
return err
}
return nil
}
2021-03-22 21:26:54 +00:00
func ( ps * postgresService ) UpdateByID ( id string , i interface { } ) error {
2021-04-19 17:42:19 +00:00
if _ , err := ps . conn . Model ( i ) . Where ( "id = ?" , id ) . OnConflict ( "(id) DO UPDATE" ) . Insert ( ) ; err != nil {
2021-04-01 18:46:45 +00:00
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-01 18:46:45 +00:00
}
return err
}
return nil
}
func ( ps * postgresService ) UpdateOneByID ( id string , key string , value interface { } , i interface { } ) error {
_ , err := ps . conn . Model ( i ) . Set ( "? = ?" , pg . Safe ( key ) , value ) . Where ( "id = ?" , id ) . Update ( )
2021-03-22 21:26:54 +00:00
return err
}
func ( ps * postgresService ) DeleteByID ( id string , i interface { } ) error {
2021-04-01 18:46:45 +00:00
if _ , err := ps . conn . Model ( i ) . Where ( "id = ?" , id ) . Delete ( ) ; err != nil {
2021-05-21 13:48:26 +00:00
// 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
2021-04-01 18:46:45 +00:00
}
}
return nil
2021-03-22 21:26:54 +00:00
}
2021-05-21 13:48:26 +00:00
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
2021-04-01 18:46:45 +00:00
}
}
return nil
}
/ *
HANDY SHORTCUTS
* /
2021-05-21 13:48:26 +00:00
func ( ps * postgresService ) AcceptFollowRequest ( originAccountID string , targetAccountID string ) ( * gtsmodel . Follow , error ) {
// make sure the original follow request exists
2021-05-15 09:58:11 +00:00
fr := & gtsmodel . FollowRequest { }
if err := ps . conn . Model ( fr ) . Where ( "account_id = ?" , originAccountID ) . Where ( "target_account_id = ?" , targetAccountID ) . Select ( ) ; err != nil {
if err == pg . ErrMultiRows {
2021-05-21 13:48:26 +00:00
return nil , db . ErrNoEntries { }
2021-05-15 09:58:11 +00:00
}
2021-05-21 13:48:26 +00:00
return nil , err
2021-05-15 09:58:11 +00:00
}
2021-05-21 13:48:26 +00:00
// create a new follow to 'replace' the request with
2021-05-15 09:58:11 +00:00
follow := & gtsmodel . Follow {
AccountID : originAccountID ,
TargetAccountID : targetAccountID ,
URI : fr . URI ,
}
2021-05-21 13:48:26 +00:00
// 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
2021-05-15 09:58:11 +00:00
}
2021-05-21 13:48:26 +00:00
// now remove the follow request
2021-05-15 09:58:11 +00:00
if _ , err := ps . conn . Model ( & gtsmodel . FollowRequest { } ) . Where ( "account_id = ?" , originAccountID ) . Where ( "target_account_id = ?" , targetAccountID ) . Delete ( ) ; err != nil {
2021-05-21 13:48:26 +00:00
return nil , err
2021-05-15 09:58:11 +00:00
}
2021-05-21 13:48:26 +00:00
return follow , nil
2021-05-15 09:58:11 +00:00
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) CreateInstanceAccount ( ) error {
username := ps . config . Host
2021-05-09 12:06:06 +00:00
key , err := rsa . GenerateKey ( rand . Reader , 2048 )
if err != nil {
ps . log . Errorf ( "error creating new rsa key: %s" , err )
return err
}
newAccountURIs := util . GenerateURIsForAccount ( username , ps . config . Protocol , ps . config . Host )
a := & gtsmodel . Account {
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 {
i := & gtsmodel . Instance {
Domain : ps . config . Host ,
2021-05-09 18:34:27 +00:00
Title : ps . config . Host ,
URI : fmt . Sprintf ( "%s://%s" , ps . config . Protocol , ps . config . Host ) ,
2021-04-19 17:42:19 +00:00
}
2021-05-09 12:06:06 +00:00
inserted , err := ps . conn . Model ( i ) . Where ( "domain = ?" , ps . config . Host ) . SelectOrInsert ( )
2021-04-19 17:42:19 +00:00
if err != nil {
return err
}
if inserted {
2021-05-09 12:06:06 +00:00
ps . log . Infof ( "created instance instance %s with id %s" , ps . config . Host , i . ID )
2021-04-19 17:42:19 +00:00
} else {
2021-05-09 18:34:27 +00:00
ps . log . Infof ( "instance instance %s already exists with id %s" , ps . config . Host , i . ID )
2021-04-19 17:42:19 +00:00
}
return nil
}
func ( ps * postgresService ) GetAccountByUserID ( userID string , account * gtsmodel . Account ) error {
user := & gtsmodel . User {
2021-04-01 18:46:45 +00:00
ID : userID ,
}
if err := ps . conn . Model ( user ) . Where ( "id = ?" , userID ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-01 18:46:45 +00:00
}
return err
}
if err := ps . conn . Model ( account ) . Where ( "id = ?" , user . AccountID ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-01 18:46:45 +00:00
}
return err
}
return nil
}
2021-05-08 12:25:55 +00:00
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 {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-05-08 12:25:55 +00:00
}
return err
}
return nil
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) GetFollowRequestsForAccountID ( accountID string , followRequests * [ ] gtsmodel . FollowRequest ) error {
2021-04-01 18:46:45 +00:00
if err := ps . conn . Model ( followRequests ) . Where ( "target_account_id = ?" , accountID ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return nil
2021-04-01 18:46:45 +00:00
}
return err
}
return nil
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) GetFollowingByAccountID ( accountID string , following * [ ] gtsmodel . Follow ) error {
2021-04-01 18:46:45 +00:00
if err := ps . conn . Model ( following ) . Where ( "account_id = ?" , accountID ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return nil
2021-04-01 18:46:45 +00:00
}
return err
}
return nil
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) GetFollowersByAccountID ( accountID string , followers * [ ] gtsmodel . Follow ) error {
2021-04-01 18:46:45 +00:00
if err := ps . conn . Model ( followers ) . Where ( "target_account_id = ?" , accountID ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
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
2021-04-01 18:46:45 +00:00
}
return err
}
return nil
}
2021-05-17 17:06:58 +00:00
func ( ps * postgresService ) CountStatusesByAccountID ( accountID string ) ( int , error ) {
count , err := ps . conn . Model ( & gtsmodel . Status { } ) . Where ( "account_id = ?" , accountID ) . Count ( )
if err != nil {
2021-04-01 18:46:45 +00:00
if err == pg . ErrNoRows {
2021-05-17 17:06:58 +00:00
return 0 , nil
2021-04-01 18:46:45 +00:00
}
2021-05-17 17:06:58 +00:00
return 0 , err
2021-04-01 18:46:45 +00:00
}
2021-05-17 17:06:58 +00:00
return count , nil
2021-04-01 18:46:45 +00:00
}
2021-05-17 17:06:58 +00:00
func ( ps * postgresService ) GetStatusesByTimeDescending ( accountID string , statuses * [ ] gtsmodel . Status , limit int , excludeReplies bool , maxID string , pinned bool , mediaOnly bool ) error {
2021-04-01 18:46:45 +00:00
q := ps . conn . Model ( statuses ) . Order ( "created_at DESC" )
2021-05-17 17:06:58 +00:00
if accountID != "" {
q = q . Where ( "account_id = ?" , accountID )
}
2021-04-01 18:46:45 +00:00
if limit != 0 {
q = q . Limit ( limit )
}
2021-05-17 17:06:58 +00:00
if excludeReplies {
q = q . Where ( "? IS NULL" , pg . Ident ( "in_reply_to_id" ) )
}
if pinned {
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
} )
2021-04-01 18:46:45 +00:00
}
2021-05-23 16:07:04 +00:00
if maxID != "" {
s := & gtsmodel . Status { }
if err := ps . conn . Model ( s ) . Where ( "id = ?" , maxID ) . Select ( ) ; err != nil {
return err
}
q = q . Where ( "status.created_at < ?" , s . CreatedAt )
}
2021-04-01 18:46:45 +00:00
if err := q . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-01 18:46:45 +00:00
}
return err
}
return nil
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) GetLastStatusForAccountID ( accountID string , status * gtsmodel . Status ) error {
2021-04-01 18:46:45 +00:00
if err := ps . conn . Model ( status ) . Order ( "created_at DESC" ) . Limit ( 1 ) . Where ( "account_id = ?" , accountID ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-01 18:46:45 +00:00
}
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
2021-04-19 17:42:19 +00:00
if err := ps . conn . Model ( & gtsmodel . Account { } ) . Where ( "username = ?" , username ) . Where ( "domain = ?" , nil ) . Select ( ) ; err == nil {
2021-04-01 18:46:45 +00:00
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
2021-04-19 17:42:19 +00:00
if err := ps . conn . Model ( & gtsmodel . EmailDomainBlock { } ) . Where ( "domain = ?" , domain ) . Select ( ) ; err == nil {
2021-04-01 18:46:45 +00:00
// 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
2021-04-19 17:42:19 +00:00
if err := ps . conn . Model ( & gtsmodel . User { } ) . Where ( "email = ?" , email ) . WhereOr ( "unconfirmed_email = ?" , email ) . Select ( ) ; err == nil {
2021-04-01 18:46:45 +00:00
// 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
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) NewSignup ( username string , reason string , requireApproval bool , email string , password string , signUpIP net . IP , locale string , appID string ) ( * gtsmodel . User , error ) {
2021-04-01 18:46:45 +00:00
key , err := rsa . GenerateKey ( rand . Reader , 2048 )
if err != nil {
ps . log . Errorf ( "error creating new rsa key: %s" , err )
return nil , err
}
2021-05-08 12:25:55 +00:00
newAccountURIs := util . GenerateURIsForAccount ( username , ps . config . Protocol , ps . config . Host )
2021-04-01 18:46:45 +00:00
2021-04-19 17:42:19 +00:00
a := & gtsmodel . Account {
2021-04-01 18:46:45 +00:00
Username : username ,
DisplayName : username ,
Reason : reason ,
2021-05-08 12:25:55 +00:00
URL : newAccountURIs . UserURL ,
2021-04-01 18:46:45 +00:00
PrivateKey : key ,
PublicKey : & key . PublicKey ,
2021-05-08 12:25:55 +00:00
PublicKeyURI : newAccountURIs . PublicKeyURI ,
2021-04-19 17:42:19 +00:00
ActorType : gtsmodel . ActivityStreamsPerson ,
2021-05-08 12:25:55 +00:00
URI : newAccountURIs . UserURI ,
InboxURI : newAccountURIs . InboxURI ,
OutboxURI : newAccountURIs . OutboxURI ,
FollowersURI : newAccountURIs . FollowersURI ,
FollowingURI : newAccountURIs . FollowingURI ,
FeaturedCollectionURI : newAccountURIs . CollectionURI ,
2021-04-01 18:46:45 +00:00
}
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 )
}
2021-04-19 17:42:19 +00:00
u := & gtsmodel . User {
2021-04-01 18:46:45 +00:00
AccountID : a . ID ,
EncryptedPassword : string ( pw ) ,
SignUpIP : signUpIP ,
Locale : locale ,
UnconfirmedEmail : email ,
CreatedByApplicationID : appID ,
Approved : ! requireApproval , // if we don't require moderator approval, just pre-approve the user
}
if _ , err = ps . conn . Model ( u ) . Insert ( ) ; err != nil {
return nil , err
}
return u , nil
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) SetHeaderOrAvatarForAccountID ( mediaAttachment * gtsmodel . MediaAttachment , accountID string ) error {
if mediaAttachment . Avatar && mediaAttachment . Header {
return errors . New ( "one media attachment cannot be both header and avatar" )
}
var headerOrAVI string
if mediaAttachment . Avatar {
headerOrAVI = "avatar"
} else if mediaAttachment . Header {
headerOrAVI = "header"
} else {
return errors . New ( "given media attachment was neither a header nor an avatar" )
}
// TODO: there are probably more side effects here that need to be handled
if _ , err := ps . conn . Model ( mediaAttachment ) . OnConflict ( "(id) DO UPDATE" ) . Insert ( ) ; err != nil {
return err
}
if _ , err := ps . conn . Model ( & gtsmodel . Account { } ) . Set ( fmt . Sprintf ( "%s_media_attachment_id = ?" , headerOrAVI ) , mediaAttachment . ID ) . Where ( "id = ?" , accountID ) . Update ( ) ; err != nil {
return err
}
return nil
2021-03-14 16:56:16 +00:00
}
2021-04-01 18:46:45 +00:00
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) GetHeaderForAccountID ( header * gtsmodel . MediaAttachment , accountID string ) error {
acct := & gtsmodel . Account { }
if err := ps . conn . Model ( acct ) . Where ( "id = ?" , accountID ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-19 17:42:19 +00:00
}
return err
}
if acct . HeaderMediaAttachmentID == "" {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-19 17:42:19 +00:00
}
if err := ps . conn . Model ( header ) . Where ( "id = ?" , acct . HeaderMediaAttachmentID ) . Select ( ) ; err != nil {
2021-04-01 18:46:45 +00:00
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-01 18:46:45 +00:00
}
return err
}
return nil
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) GetAvatarForAccountID ( avatar * gtsmodel . MediaAttachment , accountID string ) error {
acct := & gtsmodel . Account { }
if err := ps . conn . Model ( acct ) . Where ( "id = ?" , accountID ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-19 17:42:19 +00:00
}
return err
}
if acct . AvatarMediaAttachmentID == "" {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-19 17:42:19 +00:00
}
if err := ps . conn . Model ( avatar ) . Where ( "id = ?" , acct . AvatarMediaAttachmentID ) . Select ( ) ; err != nil {
2021-04-01 18:46:45 +00:00
if err == pg . ErrNoRows {
2021-05-15 09:58:11 +00:00
return db . ErrNoEntries { }
2021-04-01 18:46:45 +00:00
}
return err
}
return nil
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) Blocked ( account1 string , account2 string ) ( bool , error ) {
2021-05-08 12:25:55 +00:00
// TODO: check domain blocks as well
2021-04-19 17:42:19 +00:00
var blocked bool
if err := ps . conn . Model ( & gtsmodel . Block { } ) .
Where ( "account_id = ?" , account1 ) . Where ( "target_account_id = ?" , account2 ) .
WhereOr ( "target_account_id = ?" , account1 ) . Where ( "account_id = ?" , account2 ) .
Select ( ) ; err != nil {
if err == pg . ErrNoRows {
blocked = false
return blocked , nil
}
2021-04-20 16:14:23 +00:00
return blocked , err
2021-04-19 17:42:19 +00:00
}
blocked = true
return blocked , nil
}
2021-04-01 18:46:45 +00:00
2021-05-21 13:48:26 +00:00
func ( ps * postgresService ) GetRelationship ( requestingAccount string , targetAccount string ) ( * gtsmodel . Relationship , error ) {
r := & gtsmodel . Relationship {
ID : targetAccount ,
}
// check if the requesting account follows the target account
follow := & gtsmodel . 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 ( & gtsmodel . 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 ( & gtsmodel . 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 ( & gtsmodel . 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 ( & gtsmodel . 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
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) StatusVisible ( targetStatus * gtsmodel . Status , targetAccount * gtsmodel . Account , requestingAccount * gtsmodel . Account , relevantAccounts * gtsmodel . RelevantAccounts ) ( bool , error ) {
l := ps . log . WithField ( "func" , "StatusVisible" )
// if target account is suspended then don't show the status
if ! targetAccount . SuspendedAt . IsZero ( ) {
l . Debug ( "target account suspended at is not zero" )
return false , nil
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
// if the target user doesn't exist (anymore) then the status also shouldn't be visible
2021-05-17 17:06:58 +00:00
// note: we only do this for local users
if targetAccount . Domain == "" {
targetUser := & gtsmodel . User { }
if err := ps . conn . Model ( targetUser ) . Where ( "account_id = ?" , targetAccount . ID ) . Select ( ) ; err != nil {
l . Debug ( "target user could not be selected" )
if err == pg . ErrNoRows {
return false , db . ErrNoEntries { }
}
return false , err
2021-04-19 17:42:19 +00:00
}
2021-04-01 18:46:45 +00:00
2021-05-17 17:06:58 +00:00
// if target user is disabled, not yet approved, or not confirmed then don't show the status
// (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!)
if targetUser . Disabled || ! targetUser . Approved || targetUser . ConfirmedAt . IsZero ( ) {
l . Debug ( "target user is disabled, not approved, or not confirmed" )
return false , nil
}
2021-04-19 17:42:19 +00:00
}
// If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed.
// In this case, we can still serve the status if it's public, otherwise we definitely shouldn't.
if requestingAccount == nil {
if targetStatus . Visibility == gtsmodel . VisibilityPublic {
return true , nil
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
l . Debug ( "requesting account is nil but the target status isn't public" )
return false , nil
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
// if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten
// this far (ie., been authed) in the first place: this is just for safety.
if ! requestingAccount . SuspendedAt . IsZero ( ) {
l . Debug ( "requesting account is suspended" )
return false , nil
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
// check if we have a local account -- if so we can check the user for that account in the DB
if requestingAccount . Domain == "" {
requestingUser := & gtsmodel . User { }
if err := ps . conn . Model ( requestingUser ) . Where ( "account_id = ?" , requestingAccount . ID ) . Select ( ) ; err != nil {
// if the requesting account is local but doesn't have a corresponding user in the db this is a problem
if err == pg . ErrNoRows {
l . Debug ( "requesting account is local but there's no corresponding user" )
return false , nil
}
2021-04-20 16:14:23 +00:00
l . Debugf ( "requesting account is local but there was an error getting the corresponding user: %s" , err )
return false , err
2021-04-19 17:42:19 +00:00
}
// okay, user exists, so make sure it has full privileges/is confirmed/approved
if requestingUser . Disabled || ! requestingUser . Approved || requestingUser . ConfirmedAt . IsZero ( ) {
l . Debug ( "requesting account is local but corresponding user is either disabled, not approved, or not confirmed" )
return false , nil
}
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
// if the target status belongs to the requesting account, they should always be able to view it at this point
if targetStatus . AccountID == requestingAccount . ID {
return true , nil
}
// At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou
// First check if a block exists directly between the target account (which authored the status) and the requesting account.
if blocked , err := ps . Blocked ( targetAccount . ID , requestingAccount . ID ) ; err != nil {
l . Debugf ( "something went wrong figuring out if the accounts have a block: %s" , err )
return false , err
} else if blocked {
// don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please
l . Debug ( "a block exists between requesting account and target account" )
return false , nil
}
// check other accounts mentioned/boosted by/replied to by the status, if they exist
if relevantAccounts != nil {
// status replies to account id
if relevantAccounts . ReplyToAccount != nil {
if blocked , err := ps . Blocked ( relevantAccounts . ReplyToAccount . ID , requestingAccount . ID ) ; err != nil {
return false , err
} else if blocked {
2021-05-17 17:06:58 +00:00
l . Debug ( "a block exists between requesting account and reply to account" )
2021-04-19 17:42:19 +00:00
return false , nil
}
}
2021-04-01 18:46:45 +00:00
2021-04-19 17:42:19 +00:00
// status boosts accounts id
if relevantAccounts . BoostedAccount != nil {
if blocked , err := ps . Blocked ( relevantAccounts . BoostedAccount . ID , requestingAccount . ID ) ; err != nil {
return false , err
} else if blocked {
2021-05-17 17:06:58 +00:00
l . Debug ( "a block exists between requesting account and boosted account" )
2021-04-19 17:42:19 +00:00
return false , nil
}
}
// status boosts a reply to account id
if relevantAccounts . BoostedReplyToAccount != nil {
if blocked , err := ps . Blocked ( relevantAccounts . BoostedReplyToAccount . ID , requestingAccount . ID ) ; err != nil {
return false , err
} else if blocked {
2021-05-17 17:06:58 +00:00
l . Debug ( "a block exists between requesting account and boosted reply to account" )
2021-04-19 17:42:19 +00:00
return false , nil
}
}
// status mentions accounts
for _ , a := range relevantAccounts . MentionedAccounts {
if blocked , err := ps . Blocked ( a . ID , requestingAccount . ID ) ; err != nil {
return false , err
} else if blocked {
2021-05-17 17:06:58 +00:00
l . Debug ( "a block exists between requesting account and a mentioned account" )
2021-04-19 17:42:19 +00:00
return false , nil
}
}
2021-05-17 17:06:58 +00:00
// if the requesting account is mentioned in the status it should always be visible
for _ , acct := range relevantAccounts . MentionedAccounts {
if acct . ID == requestingAccount . ID {
return true , nil // yep it's mentioned!
}
}
2021-04-19 17:42:19 +00:00
}
// at this point we know neither account blocks the other, or another account mentioned or otherwise referred to in the status
// that means it's now just a matter of checking the visibility settings of the status itself
switch targetStatus . Visibility {
case gtsmodel . VisibilityPublic , gtsmodel . VisibilityUnlocked :
// no problem here, just return OK
return true , nil
case gtsmodel . VisibilityFollowersOnly :
// check one-way follow
follows , err := ps . Follows ( requestingAccount , targetAccount )
if err != nil {
return false , err
}
if ! follows {
2021-05-17 17:06:58 +00:00
l . Debug ( "requested status is followers only but requesting account is not a follower" )
2021-04-19 17:42:19 +00:00
return false , nil
}
return true , nil
case gtsmodel . VisibilityMutualsOnly :
// check mutual follow
mutuals , err := ps . Mutuals ( requestingAccount , targetAccount )
if err != nil {
return false , err
}
if ! mutuals {
2021-05-17 17:06:58 +00:00
l . Debug ( "requested status is mutuals only but accounts aren't mufos" )
2021-04-19 17:42:19 +00:00
return false , nil
}
return true , nil
case gtsmodel . VisibilityDirect :
2021-05-17 17:06:58 +00:00
l . Debug ( "requesting account requests a status it's not mentioned in" )
2021-04-19 17:42:19 +00:00
return false , nil // it's not mentioned -_-
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
return false , errors . New ( "reached the end of StatusVisible with no result" )
}
func ( ps * postgresService ) Follows ( sourceAccount * gtsmodel . Account , targetAccount * gtsmodel . Account ) ( bool , error ) {
return ps . conn . Model ( & gtsmodel . Follow { } ) . Where ( "account_id = ?" , sourceAccount . ID ) . Where ( "target_account_id = ?" , targetAccount . ID ) . Exists ( )
}
2021-05-21 13:48:26 +00:00
func ( ps * postgresService ) FollowRequested ( sourceAccount * gtsmodel . Account , targetAccount * gtsmodel . Account ) ( bool , error ) {
return ps . conn . Model ( & gtsmodel . FollowRequest { } ) . Where ( "account_id = ?" , sourceAccount . ID ) . Where ( "target_account_id = ?" , targetAccount . ID ) . Exists ( )
}
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) Mutuals ( account1 * gtsmodel . Account , account2 * gtsmodel . Account ) ( bool , error ) {
// make sure account 1 follows account 2
f1 , err := ps . conn . Model ( & gtsmodel . Follow { } ) . Where ( "account_id = ?" , account1 . ID ) . Where ( "target_account_id = ?" , account2 . ID ) . Exists ( )
if err != nil {
if err == pg . ErrNoRows {
return false , nil
}
2021-04-20 16:14:23 +00:00
return false , err
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
// make sure account 2 follows account 1
f2 , err := ps . conn . Model ( & gtsmodel . Follow { } ) . Where ( "account_id = ?" , account2 . ID ) . Where ( "target_account_id = ?" , account1 . ID ) . Exists ( )
if err != nil {
if err == pg . ErrNoRows {
return false , nil
2021-04-01 18:46:45 +00:00
}
2021-04-20 16:14:23 +00:00
return false , err
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
return f1 && f2 , nil
}
func ( ps * postgresService ) PullRelevantAccountsFromStatus ( targetStatus * gtsmodel . Status ) ( * gtsmodel . RelevantAccounts , error ) {
accounts := & gtsmodel . RelevantAccounts {
MentionedAccounts : [ ] * gtsmodel . Account { } ,
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
// get the replied to account from the status and add it to the pile
if targetStatus . InReplyToAccountID != "" {
repliedToAccount := & gtsmodel . Account { }
if err := ps . conn . Model ( repliedToAccount ) . Where ( "id = ?" , targetStatus . InReplyToAccountID ) . Select ( ) ; err != nil {
2021-05-28 17:57:04 +00:00
return accounts , fmt . Errorf ( "PullRelevantAccountsFromStatus: error getting repliedToAcount with id %s: %s" , targetStatus . InReplyToAccountID , err )
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
accounts . ReplyToAccount = repliedToAccount
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
// get the boosted account from the status and add it to the pile
if targetStatus . BoostOfID != "" {
// retrieve the boosted status first
boostedStatus := & gtsmodel . Status { }
if err := ps . conn . Model ( boostedStatus ) . Where ( "id = ?" , targetStatus . BoostOfID ) . Select ( ) ; err != nil {
2021-05-28 17:57:04 +00:00
return accounts , fmt . Errorf ( "PullRelevantAccountsFromStatus: error getting boostedStatus with id %s: %s" , targetStatus . BoostOfID , err )
2021-04-19 17:42:19 +00:00
}
boostedAccount := & gtsmodel . Account { }
if err := ps . conn . Model ( boostedAccount ) . Where ( "id = ?" , boostedStatus . AccountID ) . Select ( ) ; err != nil {
2021-05-28 17:57:04 +00:00
return accounts , fmt . Errorf ( "PullRelevantAccountsFromStatus: error getting boostedAccount with id %s: %s" , boostedStatus . AccountID , err )
2021-04-19 17:42:19 +00:00
}
accounts . BoostedAccount = boostedAccount
// the boosted status might be a reply to another account so we should get that too
if boostedStatus . InReplyToAccountID != "" {
boostedStatusRepliedToAccount := & gtsmodel . Account { }
if err := ps . conn . Model ( boostedStatusRepliedToAccount ) . Where ( "id = ?" , boostedStatus . InReplyToAccountID ) . Select ( ) ; err != nil {
2021-05-28 17:57:04 +00:00
return accounts , fmt . Errorf ( "PullRelevantAccountsFromStatus: error getting boostedStatusRepliedToAccount with id %s: %s" , boostedStatus . InReplyToAccountID , err )
2021-04-19 17:42:19 +00:00
}
accounts . BoostedReplyToAccount = boostedStatusRepliedToAccount
}
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
// now get all accounts with IDs that are mentioned in the status
2021-05-17 17:06:58 +00:00
for _ , mentionID := range targetStatus . Mentions {
mention := & gtsmodel . Mention { }
if err := ps . conn . Model ( mention ) . Where ( "id = ?" , mentionID ) . Select ( ) ; err != nil {
2021-05-28 17:57:04 +00:00
return accounts , fmt . Errorf ( "PullRelevantAccountsFromStatus: error getting mention with id %s: %s" , mentionID , err )
2021-05-17 17:06:58 +00:00
}
2021-04-19 17:42:19 +00:00
mentionedAccount := & gtsmodel . Account { }
2021-05-17 17:06:58 +00:00
if err := ps . conn . Model ( mentionedAccount ) . Where ( "id = ?" , mention . TargetAccountID ) . Select ( ) ; err != nil {
2021-05-28 17:57:04 +00:00
return accounts , fmt . Errorf ( "PullRelevantAccountsFromStatus: error getting mentioned account: %s" , err )
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
accounts . MentionedAccounts = append ( accounts . MentionedAccounts , mentionedAccount )
}
return accounts , nil
}
func ( ps * postgresService ) GetReplyCountForStatus ( status * gtsmodel . Status ) ( int , error ) {
return ps . conn . Model ( & gtsmodel . Status { } ) . Where ( "in_reply_to_id = ?" , status . ID ) . Count ( )
}
func ( ps * postgresService ) GetReblogCountForStatus ( status * gtsmodel . Status ) ( int , error ) {
return ps . conn . Model ( & gtsmodel . Status { } ) . Where ( "boost_of_id = ?" , status . ID ) . Count ( )
}
func ( ps * postgresService ) GetFaveCountForStatus ( status * gtsmodel . Status ) ( int , error ) {
return ps . conn . Model ( & gtsmodel . StatusFave { } ) . Where ( "status_id = ?" , status . ID ) . Count ( )
}
func ( ps * postgresService ) StatusFavedBy ( status * gtsmodel . Status , accountID string ) ( bool , error ) {
return ps . conn . Model ( & gtsmodel . StatusFave { } ) . Where ( "status_id = ?" , status . ID ) . Where ( "account_id = ?" , accountID ) . Exists ( )
}
func ( ps * postgresService ) StatusRebloggedBy ( status * gtsmodel . Status , accountID string ) ( bool , error ) {
return ps . conn . Model ( & gtsmodel . Status { } ) . Where ( "boost_of_id = ?" , status . ID ) . Where ( "account_id = ?" , accountID ) . Exists ( )
}
func ( ps * postgresService ) StatusMutedBy ( status * gtsmodel . Status , accountID string ) ( bool , error ) {
return ps . conn . Model ( & gtsmodel . StatusMute { } ) . Where ( "status_id = ?" , status . ID ) . Where ( "account_id = ?" , accountID ) . Exists ( )
}
func ( ps * postgresService ) StatusBookmarkedBy ( status * gtsmodel . Status , accountID string ) ( bool , error ) {
return ps . conn . Model ( & gtsmodel . StatusBookmark { } ) . Where ( "status_id = ?" , status . ID ) . Where ( "account_id = ?" , accountID ) . Exists ( )
}
2021-05-24 16:49:48 +00:00
// func (ps *postgresService) FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) {
// // first check if a fave already exists, we can just return if so
// existingFave := >smodel.StatusFave{}
// err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select()
// if err == nil {
// // fave already exists so just return nothing at all
// return nil, nil
// }
// // an error occurred so it might exist or not, we don't know
// if err != pg.ErrNoRows {
// return nil, err
// }
// // it doesn't exist so create it
// newFave := >smodel.StatusFave{
// AccountID: accountID,
// TargetAccountID: status.AccountID,
// StatusID: status.ID,
// }
// if _, err = ps.conn.Model(newFave).Insert(); err != nil {
// return nil, err
// }
// return newFave, nil
// }
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) UnfaveStatus ( status * gtsmodel . Status , accountID string ) ( * gtsmodel . StatusFave , error ) {
// if a fave doesn't exist, we don't need to do anything
existingFave := & gtsmodel . StatusFave { }
err := ps . conn . Model ( existingFave ) . Where ( "status_id = ?" , status . ID ) . Where ( "account_id = ?" , accountID ) . Select ( )
// the fave doesn't exist so return nothing at all
if err == pg . ErrNoRows {
return nil , nil
}
// an error occurred so it might exist or not, we don't know
if err != nil && err != pg . ErrNoRows {
return nil , err
}
// the fave exists so remove it
if _ , err = ps . conn . Model ( & gtsmodel . StatusFave { } ) . Where ( "status_id = ?" , status . ID ) . Where ( "account_id = ?" , accountID ) . Delete ( ) ; err != nil {
return nil , err
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
return existingFave , nil
}
func ( ps * postgresService ) WhoFavedStatus ( status * gtsmodel . Status ) ( [ ] * gtsmodel . Account , error ) {
accounts := [ ] * gtsmodel . Account { }
faves := [ ] * gtsmodel . StatusFave { }
if err := ps . conn . Model ( & faves ) . Where ( "status_id = ?" , status . ID ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
return accounts , nil // no rows just means nobody has faved this status, so that's fine
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
return nil , err // an actual error has occurred
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
for _ , f := range faves {
acc := & gtsmodel . Account { }
if err := ps . conn . Model ( acc ) . Where ( "id = ?" , f . AccountID ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it
}
return nil , err // an actual error has occurred
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
accounts = append ( accounts , acc )
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
return accounts , nil
}
2021-05-21 21:04:59 +00:00
func ( ps * postgresService ) GetHomeTimelineForAccount ( accountID string , maxID string , sinceID string , minID string , limit int , local bool ) ( [ ] * gtsmodel . Status , error ) {
statuses := [ ] * gtsmodel . Status { }
q := ps . conn . Model ( & statuses ) .
ColumnExpr ( "status.*" ) .
Join ( "JOIN follows AS f ON f.target_account_id = status.account_id" ) .
Where ( "f.account_id = ?" , accountID ) .
Limit ( limit ) .
Order ( "status.created_at DESC" )
2021-05-23 16:07:04 +00:00
if maxID != "" {
s := & gtsmodel . Status { }
if err := ps . conn . Model ( s ) . Where ( "id = ?" , maxID ) . Select ( ) ; err != nil {
return nil , err
}
q = q . Where ( "status.created_at < ?" , s . CreatedAt )
}
2021-05-21 21:04:59 +00:00
err := q . Select ( )
if err != nil {
if err != pg . ErrNoRows {
return nil , err
}
}
return statuses , nil
}
2021-05-27 14:06:24 +00:00
func ( ps * postgresService ) GetNotificationsForAccount ( accountID string , limit int , maxID string ) ( [ ] * gtsmodel . Notification , error ) {
notifications := [ ] * gtsmodel . Notification { }
q := ps . conn . Model ( & notifications ) . Where ( "target_account_id = ?" , accountID )
if maxID != "" {
n := & gtsmodel . Notification { }
if err := ps . conn . Model ( n ) . Where ( "id = ?" , maxID ) . Select ( ) ; err != nil {
return nil , err
}
q = q . Where ( "created_at < ?" , n . CreatedAt )
}
if limit != 0 {
q = q . Limit ( limit )
}
q = q . Order ( "created_at DESC" )
if err := q . Select ( ) ; err != nil {
if err != pg . ErrNoRows {
return nil , err
}
}
return notifications , nil
}
2021-04-19 17:42:19 +00:00
/ *
CONVERSION FUNCTIONS
* /
2021-04-01 18:46:45 +00:00
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) MentionStringsToMentions ( targetAccounts [ ] string , originAccountID string , statusID string ) ( [ ] * gtsmodel . Mention , error ) {
2021-05-21 13:48:26 +00:00
ogAccount := & gtsmodel . Account { }
if err := ps . conn . Model ( ogAccount ) . Where ( "id = ?" , originAccountID ) . Select ( ) ; err != nil {
return nil , err
}
2021-04-19 17:42:19 +00:00
menchies := [ ] * gtsmodel . Mention { }
for _ , a := range targetAccounts {
// A mentioned account looks like "@test@example.org" or just "@test" for a local account
// -- we can guarantee this from the regex that targetAccounts should have been derived from.
// But we still need to do a bit of fiddling to get what we need here -- the username and domain (if given).
// 1. trim off the first @
t := strings . TrimPrefix ( a , "@" )
// 2. split the username and domain
s := strings . Split ( t , "@" )
// 3. if it's length 1 it's a local account, length 2 means remote, anything else means something is wrong
var local bool
switch len ( s ) {
case 1 :
local = true
case 2 :
local = false
default :
return nil , fmt . Errorf ( "mentioned account format '%s' was not valid" , a )
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
var username , domain string
username = s [ 0 ]
if ! local {
domain = s [ 1 ]
}
// 4. check we now have a proper username and domain
if username == "" || ( ! local && domain == "" ) {
return nil , fmt . Errorf ( "username or domain for '%s' was nil" , a )
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
// okay we're good now, we can start pulling accounts out of the database
mentionedAccount := & gtsmodel . Account { }
var err error
if local {
// local user -- should have a null domain
err = ps . conn . Model ( mentionedAccount ) . Where ( "username = ?" , username ) . Where ( "? IS NULL" , pg . Ident ( "domain" ) ) . Select ( )
} else {
// remote user -- should have domain defined
err = ps . conn . Model ( mentionedAccount ) . Where ( "username = ?" , username ) . Where ( "? = ?" , pg . Ident ( "domain" ) , domain ) . Select ( )
}
if err != nil {
if err == pg . ErrNoRows {
// no result found for this username/domain so just don't include it as a mencho and carry on about our business
ps . log . Debugf ( "no account found with username '%s' and domain '%s', skipping it" , username , domain )
continue
}
// a serious error has happened so bail
return nil , fmt . Errorf ( "error getting account with username '%s' and domain '%s': %s" , username , domain , err )
}
// id, createdAt and updatedAt will be populated by the db, so we have everything we need!
menchies = append ( menchies , & gtsmodel . Mention {
2021-05-21 13:48:26 +00:00
StatusID : statusID ,
OriginAccountID : ogAccount . ID ,
OriginAccountURI : ogAccount . URI ,
TargetAccountID : mentionedAccount . ID ,
NameString : a ,
MentionedAccountURI : mentionedAccount . URI ,
GTSAccount : mentionedAccount ,
2021-04-19 17:42:19 +00:00
} )
2021-04-01 18:46:45 +00:00
}
2021-04-19 17:42:19 +00:00
return menchies , nil
}
2021-04-01 18:46:45 +00:00
2021-04-19 17:42:19 +00:00
func ( ps * postgresService ) TagStringsToTags ( tags [ ] string , originAccountID string , statusID string ) ( [ ] * gtsmodel . Tag , error ) {
newTags := [ ] * gtsmodel . Tag { }
for _ , t := range tags {
tag := & gtsmodel . Tag { }
// we can use selectorinsert here to create the new tag if it doesn't exist already
// inserted will be true if this is a new tag we just created
if err := ps . conn . Model ( tag ) . Where ( "name = ?" , t ) . Select ( ) ; err != nil {
if err == pg . ErrNoRows {
// tag doesn't exist yet so populate it
tag . ID = uuid . NewString ( )
tag . Name = t
tag . FirstSeenFromAccountID = originAccountID
tag . CreatedAt = time . Now ( )
tag . UpdatedAt = time . Now ( )
tag . Useable = true
tag . Listable = true
} else {
return nil , fmt . Errorf ( "error getting tag with name %s: %s" , t , err )
}
}
// bail already if the tag isn't useable
if ! tag . Useable {
continue
}
tag . LastStatusAt = time . Now ( )
newTags = append ( newTags , tag )
}
return newTags , nil
}
func ( ps * postgresService ) EmojiStringsToEmojis ( emojis [ ] string , originAccountID string , statusID string ) ( [ ] * gtsmodel . Emoji , error ) {
newEmojis := [ ] * gtsmodel . Emoji { }
for _ , e := range emojis {
emoji := & gtsmodel . Emoji { }
err := ps . conn . Model ( emoji ) . Where ( "shortcode = ?" , e ) . Where ( "visible_in_picker = true" ) . Where ( "disabled = false" ) . Select ( )
if err != nil {
if err == pg . ErrNoRows {
// no result found for this username/domain so just don't include it as an emoji and carry on about our business
ps . log . Debugf ( "no emoji found with shortcode %s, skipping it" , e )
continue
}
// a serious error has happened so bail
return nil , fmt . Errorf ( "error getting emoji with shortcode %s: %s" , e , err )
}
newEmojis = append ( newEmojis , emoji )
}
return newEmojis , nil
2021-04-01 18:46:45 +00:00
}