mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 09:52:26 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			1473 lines
		
	
	
	
		
			48 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1473 lines
		
	
	
	
		
			48 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
|    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 (
 | |
| 	"context"
 | |
| 	"crypto/rand"
 | |
| 	"crypto/rsa"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net"
 | |
| 	"net/mail"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/go-pg/pg/extra/pgdebug"
 | |
| 	"github.com/go-pg/pg/v10"
 | |
| 	"github.com/go-pg/pg/v10/orm"
 | |
| 	"github.com/google/uuid"
 | |
| 	"github.com/sirupsen/logrus"
 | |
| 	"github.com/superseriousbusiness/gotosocial/internal/config"
 | |
| 	"github.com/superseriousbusiness/gotosocial/internal/db"
 | |
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | |
| 	"github.com/superseriousbusiness/gotosocial/internal/util"
 | |
| 	"golang.org/x/crypto/bcrypt"
 | |
| )
 | |
| 
 | |
| // postgresService satisfies the DB interface
 | |
| type postgresService struct {
 | |
| 	config *config.Config
 | |
| 	conn   *pg.DB
 | |
| 	log    *logrus.Logger
 | |
| 	cancel context.CancelFunc
 | |
| 	// federationDB pub.Database
 | |
| }
 | |
| 
 | |
| // NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface.
 | |
| // Under the hood, it uses https://github.com/go-pg/pg to create and maintain a database connection.
 | |
| func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logger) (db.DB, error) {
 | |
| 	opts, err := derivePGOptions(c)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("could not create postgres service: %s", err)
 | |
| 	}
 | |
| 	log.Debugf("using pg options: %+v", opts)
 | |
| 
 | |
| 	// create a connection
 | |
| 	pgCtx, cancel := context.WithCancel(ctx)
 | |
| 	conn := pg.Connect(opts).WithContext(pgCtx)
 | |
| 
 | |
| 	// 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!
 | |
| 	if log.GetLevel() >= logrus.TraceLevel {
 | |
| 		conn.AddQueryHook(pgdebug.DebugHook{
 | |
| 			// Print all queries.
 | |
| 			Verbose: true,
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	// actually *begin* the connection so that we can tell if the db is there and listening
 | |
| 	if err := conn.Ping(ctx); err != nil {
 | |
| 		cancel()
 | |
| 		return nil, fmt.Errorf("db connection error: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	// print out discovered postgres version
 | |
| 	var version string
 | |
| 	if _, err = conn.QueryOneContext(ctx, pg.Scan(&version), "SELECT version()"); err != nil {
 | |
| 		cancel()
 | |
| 		return nil, fmt.Errorf("db connection error: %s", err)
 | |
| 	}
 | |
| 	log.Infof("connected to postgres version: %s", version)
 | |
| 
 | |
| 	ps := &postgresService{
 | |
| 		config: c,
 | |
| 		conn:   conn,
 | |
| 		log:    log,
 | |
| 		cancel: cancel,
 | |
| 	}
 | |
| 
 | |
| 	// we can confidently return this useable postgres service now
 | |
| 	return ps, nil
 | |
| }
 | |
| 
 | |
| /*
 | |
| 	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.
 | |
| func derivePGOptions(c *config.Config) (*pg.Options, error) {
 | |
| 	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)
 | |
| 	}
 | |
| 
 | |
| 	// validate port
 | |
| 	if c.DBConfig.Port == 0 {
 | |
| 		return nil, errors.New("no port set")
 | |
| 	}
 | |
| 
 | |
| 	// validate address
 | |
| 	if c.DBConfig.Address == "" {
 | |
| 		return nil, errors.New("no address set")
 | |
| 	}
 | |
| 
 | |
| 	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)
 | |
| 	}
 | |
| 
 | |
| 	// validate username
 | |
| 	if c.DBConfig.User == "" {
 | |
| 		return nil, errors.New("no user set")
 | |
| 	}
 | |
| 
 | |
| 	// validate that there's a password
 | |
| 	if c.DBConfig.Password == "" {
 | |
| 		return nil, errors.New("no password set")
 | |
| 	}
 | |
| 
 | |
| 	// validate database
 | |
| 	if c.DBConfig.Database == "" {
 | |
| 		return nil, errors.New("no database set")
 | |
| 	}
 | |
| 
 | |
| 	// We can rely on the pg library we're using to set
 | |
| 	// sensible defaults for everything we don't set here.
 | |
| 	options := &pg.Options{
 | |
| 		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,
 | |
| 	}
 | |
| 
 | |
| 	return options, nil
 | |
| }
 | |
| 
 | |
| /*
 | |
| 	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,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| 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
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) IsHealthy(ctx context.Context) error {
 | |
| 	return ps.conn.Ping(ctx)
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) CreateSchema(ctx context.Context) error {
 | |
| 	models := []interface{}{
 | |
| 		(*gtsmodel.Account)(nil),
 | |
| 		(*gtsmodel.Status)(nil),
 | |
| 		(*gtsmodel.User)(nil),
 | |
| 	}
 | |
| 	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
 | |
| }
 | |
| 
 | |
| 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 {
 | |
| 			return db.ErrNoEntries{}
 | |
| 		}
 | |
| 		return err
 | |
| 
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| 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 {
 | |
| 
 | |
| 		if w.Value == nil {
 | |
| 			q = q.Where("? IS NULL", pg.Ident(w.Key))
 | |
| 		} else {
 | |
| 			if w.CaseInsensitive {
 | |
| 				q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value)
 | |
| 			} else {
 | |
| 				q = q.Where("? = ?", pg.Safe(w.Key), w.Value)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if err := q.Select(); err != nil {
 | |
| 		if err == pg.ErrNoRows {
 | |
| 			return db.ErrNoEntries{}
 | |
| 		}
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // func (ps *postgresService) GetWhereMany(i interface{}, where ...model.Where) error {
 | |
| // 	return nil
 | |
| // }
 | |
| 
 | |
| func (ps *postgresService) GetAll(i interface{}) error {
 | |
| 	if err := ps.conn.Model(i).Select(); err != nil {
 | |
| 		if err == pg.ErrNoRows {
 | |
| 			return db.ErrNoEntries{}
 | |
| 		}
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| 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) Upsert(i interface{}, conflictColumn string) error {
 | |
| 	if _, err := ps.conn.Model(i).OnConflict(fmt.Sprintf("(%s) DO UPDATE", conflictColumn)).Insert(); err != nil {
 | |
| 		if err == pg.ErrNoRows {
 | |
| 			return db.ErrNoEntries{}
 | |
| 		}
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) UpdateByID(id string, i interface{}) error {
 | |
| 	if _, err := ps.conn.Model(i).Where("id = ?", id).OnConflict("(id) DO UPDATE").Insert(); err != nil {
 | |
| 		if err == pg.ErrNoRows {
 | |
| 			return db.ErrNoEntries{}
 | |
| 		}
 | |
| 		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()
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| 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
 | |
| }
 | |
| 
 | |
| /*
 | |
| 	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{
 | |
| 		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
 | |
| 	}
 | |
| 
 | |
| 	newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host)
 | |
| 	a := >smodel.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 := >smodel.Instance{
 | |
| 		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) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) error {
 | |
| 	q := ps.conn.Model(statuses).Order("created_at 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 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
 | |
| 		})
 | |
| 	}
 | |
| 	if maxID != "" {
 | |
| 		s := >smodel.Status{}
 | |
| 		if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		q = q.Where("status.created_at < ?", s.CreatedAt)
 | |
| 	}
 | |
| 	if err := q.Select(); err != nil {
 | |
| 		if err == pg.ErrNoRows {
 | |
| 			return db.ErrNoEntries{}
 | |
| 		}
 | |
| 		return err
 | |
| 	}
 | |
| 	return 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) (*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
 | |
| 	}
 | |
| 
 | |
| 	newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host)
 | |
| 
 | |
| 	a := >smodel.Account{
 | |
| 		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)
 | |
| 	}
 | |
| 	u := >smodel.User{
 | |
| 		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
 | |
| }
 | |
| 
 | |
| 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) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) {
 | |
| 	l := ps.log.WithField("func", "StatusVisible")
 | |
| 
 | |
| 	// if target account is suspended then don't show the status
 | |
| 	if !targetAccount.SuspendedAt.IsZero() {
 | |
| 		l.Debug("target account suspended at is not zero")
 | |
| 		return false, nil
 | |
| 	}
 | |
| 
 | |
| 	// if the target user doesn't exist (anymore) then the status also shouldn't be visible
 | |
| 	// note: we only do this for local users
 | |
| 	if targetAccount.Domain == "" {
 | |
| 		targetUser := >smodel.User{}
 | |
| 		if err := ps.conn.Model(targetUser).Where("account_id = ?", targetAccount.ID).Select(); err != nil {
 | |
| 			l.Debug("target user could not be selected")
 | |
| 			if err == pg.ErrNoRows {
 | |
| 				return false, db.ErrNoEntries{}
 | |
| 			}
 | |
| 			return false, err
 | |
| 		}
 | |
| 
 | |
| 		// if target user is disabled, not yet approved, or not confirmed then don't show the status
 | |
| 		// (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!)
 | |
| 		if targetUser.Disabled || !targetUser.Approved || targetUser.ConfirmedAt.IsZero() {
 | |
| 			l.Debug("target user is disabled, not approved, or not confirmed")
 | |
| 			return false, nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed.
 | |
| 	// In this case, we can still serve the status if it's public, otherwise we definitely shouldn't.
 | |
| 	if requestingAccount == nil {
 | |
| 		if targetStatus.Visibility == gtsmodel.VisibilityPublic {
 | |
| 			return true, nil
 | |
| 		}
 | |
| 		l.Debug("requesting account is nil but the target status isn't public")
 | |
| 		return false, nil
 | |
| 	}
 | |
| 
 | |
| 	// if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten
 | |
| 	// this far (ie., been authed) in the first place: this is just for safety.
 | |
| 	if !requestingAccount.SuspendedAt.IsZero() {
 | |
| 		l.Debug("requesting account is suspended")
 | |
| 		return false, nil
 | |
| 	}
 | |
| 
 | |
| 	// check if we have a local account -- if so we can check the user for that account in the DB
 | |
| 	if requestingAccount.Domain == "" {
 | |
| 		requestingUser := >smodel.User{}
 | |
| 		if err := ps.conn.Model(requestingUser).Where("account_id = ?", requestingAccount.ID).Select(); err != nil {
 | |
| 			// if the requesting account is local but doesn't have a corresponding user in the db this is a problem
 | |
| 			if err == pg.ErrNoRows {
 | |
| 				l.Debug("requesting account is local but there's no corresponding user")
 | |
| 				return false, nil
 | |
| 			}
 | |
| 			l.Debugf("requesting account is local but there was an error getting the corresponding user: %s", err)
 | |
| 			return false, err
 | |
| 		}
 | |
| 		// okay, user exists, so make sure it has full privileges/is confirmed/approved
 | |
| 		if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() {
 | |
| 			l.Debug("requesting account is local but corresponding user is either disabled, not approved, or not confirmed")
 | |
| 			return false, nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// if the target status belongs to the requesting account, they should always be able to view it at this point
 | |
| 	if targetStatus.AccountID == requestingAccount.ID {
 | |
| 		return true, nil
 | |
| 	}
 | |
| 
 | |
| 	// At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou
 | |
| 	// First check if a block exists directly between the target account (which authored the status) and the requesting account.
 | |
| 	if blocked, err := ps.Blocked(targetAccount.ID, requestingAccount.ID); err != nil {
 | |
| 		l.Debugf("something went wrong figuring out if the accounts have a block: %s", err)
 | |
| 		return false, err
 | |
| 	} else if blocked {
 | |
| 		// don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please
 | |
| 		l.Debug("a block exists between requesting account and target account")
 | |
| 		return false, nil
 | |
| 	}
 | |
| 
 | |
| 	// check other accounts mentioned/boosted by/replied to by the status, if they exist
 | |
| 	if relevantAccounts != nil {
 | |
| 		// status replies to account id
 | |
| 		if relevantAccounts.ReplyToAccount != nil {
 | |
| 			if blocked, err := ps.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil {
 | |
| 				return false, err
 | |
| 			} else if blocked {
 | |
| 				l.Debug("a block exists between requesting account and reply to account")
 | |
| 				return false, nil
 | |
| 			}
 | |
| 
 | |
| 			// check reply to ID
 | |
| 			if targetStatus.InReplyToID != "" {
 | |
| 				followsRepliedAccount, err := ps.Follows(requestingAccount, relevantAccounts.ReplyToAccount)
 | |
| 				if err != nil {
 | |
| 					return false, err
 | |
| 				}
 | |
| 				if !followsRepliedAccount {
 | |
| 					l.Debug("target status is a followers-only reply to an account that is not followed by the requesting account")
 | |
| 					return false, nil
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// status boosts accounts id
 | |
| 		if relevantAccounts.BoostedAccount != nil {
 | |
| 			if blocked, err := ps.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil {
 | |
| 				return false, err
 | |
| 			} else if blocked {
 | |
| 				l.Debug("a block exists between requesting account and boosted account")
 | |
| 				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 {
 | |
| 				l.Debug("a block exists between requesting account and boosted reply to account")
 | |
| 				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 {
 | |
| 				l.Debug("a block exists between requesting account and a mentioned account")
 | |
| 				return false, nil
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// 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!
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// 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 {
 | |
| 			l.Debug("requested status is followers only but requesting account is not a follower")
 | |
| 			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 {
 | |
| 			l.Debug("requested status is mutuals only but accounts aren't mufos")
 | |
| 			return false, nil
 | |
| 		}
 | |
| 		return true, nil
 | |
| 	case gtsmodel.VisibilityDirect:
 | |
| 		l.Debug("requesting account requests a status it's not mentioned in")
 | |
| 		return false, nil // it's not mentioned -_-
 | |
| 	}
 | |
| 
 | |
| 	return false, errors.New("reached the end of StatusVisible with no result")
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) {
 | |
| 	return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) {
 | |
| 	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) {
 | |
| 	// 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) PullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error) {
 | |
| 	accounts := >smodel.RelevantAccounts{
 | |
| 		MentionedAccounts: []*gtsmodel.Account{},
 | |
| 	}
 | |
| 
 | |
| 	// get the author account
 | |
| 	if targetStatus.GTSAuthorAccount == nil {
 | |
| 		statusAuthor := >smodel.Account{}
 | |
| 		if err := ps.conn.Model(statusAuthor).Where("id = ?", targetStatus.AccountID).Select(); err != nil {
 | |
| 			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting statusAuthor with id %s: %s", targetStatus.AccountID, err)
 | |
| 		}
 | |
| 		targetStatus.GTSAuthorAccount = statusAuthor
 | |
| 	}
 | |
| 	accounts.StatusAuthor = targetStatus.GTSAuthorAccount
 | |
| 
 | |
| 	// get the replied to account from the status and add it to the pile
 | |
| 	if targetStatus.InReplyToAccountID != "" {
 | |
| 		repliedToAccount := >smodel.Account{}
 | |
| 		if err := ps.conn.Model(repliedToAccount).Where("id = ?", targetStatus.InReplyToAccountID).Select(); err != nil {
 | |
| 			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting repliedToAcount with id %s: %s", targetStatus.InReplyToAccountID, err)
 | |
| 		}
 | |
| 		accounts.ReplyToAccount = repliedToAccount
 | |
| 	}
 | |
| 
 | |
| 	// get the boosted account from the status and add it to the pile
 | |
| 	if targetStatus.BoostOfID != "" {
 | |
| 		// retrieve the boosted status first
 | |
| 		boostedStatus := >smodel.Status{}
 | |
| 		if err := ps.conn.Model(boostedStatus).Where("id = ?", targetStatus.BoostOfID).Select(); err != nil {
 | |
| 			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatus with id %s: %s", targetStatus.BoostOfID, err)
 | |
| 		}
 | |
| 		boostedAccount := >smodel.Account{}
 | |
| 		if err := ps.conn.Model(boostedAccount).Where("id = ?", boostedStatus.AccountID).Select(); err != nil {
 | |
| 			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedAccount with id %s: %s", boostedStatus.AccountID, err)
 | |
| 		}
 | |
| 		accounts.BoostedAccount = boostedAccount
 | |
| 
 | |
| 		// the boosted status might be a reply to another account so we should get that too
 | |
| 		if boostedStatus.InReplyToAccountID != "" {
 | |
| 			boostedStatusRepliedToAccount := >smodel.Account{}
 | |
| 			if err := ps.conn.Model(boostedStatusRepliedToAccount).Where("id = ?", boostedStatus.InReplyToAccountID).Select(); err != nil {
 | |
| 				return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting boostedStatusRepliedToAccount with id %s: %s", boostedStatus.InReplyToAccountID, err)
 | |
| 			}
 | |
| 			accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// now get all accounts with IDs that are mentioned in the status
 | |
| 	for _, mentionID := range targetStatus.Mentions {
 | |
| 
 | |
| 		mention := >smodel.Mention{}
 | |
| 		if err := ps.conn.Model(mention).Where("id = ?", mentionID).Select(); err != nil {
 | |
| 			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting mention with id %s: %s", mentionID, err)
 | |
| 		}
 | |
| 
 | |
| 		mentionedAccount := >smodel.Account{}
 | |
| 		if err := ps.conn.Model(mentionedAccount).Where("id = ?", mention.TargetAccountID).Select(); err != nil {
 | |
| 			return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting mentioned account: %s", err)
 | |
| 		}
 | |
| 		accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount)
 | |
| 	}
 | |
| 
 | |
| 	return accounts, nil
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) GetReplyCountForStatus(status *gtsmodel.Status) (int, error) {
 | |
| 	return ps.conn.Model(>smodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count()
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) GetReblogCountForStatus(status *gtsmodel.Status) (int, error) {
 | |
| 	return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Count()
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) GetFaveCountForStatus(status *gtsmodel.Status) (int, error) {
 | |
| 	return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Count()
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) {
 | |
| 	return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) {
 | |
| 	return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) {
 | |
| 	return ps.conn.Model(>smodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) {
 | |
| 	return ps.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
 | |
| }
 | |
| 
 | |
| // func (ps *postgresService) FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) {
 | |
| // 	// first check if a fave already exists, we can just return if so
 | |
| // 	existingFave := >smodel.StatusFave{}
 | |
| // 	err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select()
 | |
| // 	if err == nil {
 | |
| // 		// fave already exists so just return nothing at all
 | |
| // 		return nil, nil
 | |
| // 	}
 | |
| 
 | |
| // 	// an error occurred so it might exist or not, we don't know
 | |
| // 	if err != pg.ErrNoRows {
 | |
| // 		return nil, err
 | |
| // 	}
 | |
| 
 | |
| // 	// it doesn't exist so create it
 | |
| // 	newFave := >smodel.StatusFave{
 | |
| // 		AccountID:       accountID,
 | |
| // 		TargetAccountID: status.AccountID,
 | |
| // 		StatusID:        status.ID,
 | |
| // 	}
 | |
| // 	if _, err = ps.conn.Model(newFave).Insert(); err != nil {
 | |
| // 		return nil, err
 | |
| // 	}
 | |
| 
 | |
| // 	return newFave, nil
 | |
| // }
 | |
| 
 | |
| func (ps *postgresService) UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) {
 | |
| 	// if a fave doesn't exist, we don't need to do anything
 | |
| 	existingFave := >smodel.StatusFave{}
 | |
| 	err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select()
 | |
| 	// the fave doesn't exist so return nothing at all
 | |
| 	if err == pg.ErrNoRows {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	// an error occurred so it might exist or not, we don't know
 | |
| 	if err != nil && err != pg.ErrNoRows {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// the fave exists so remove it
 | |
| 	if _, err = ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Delete(); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return existingFave, nil
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) {
 | |
| 	accounts := []*gtsmodel.Account{}
 | |
| 
 | |
| 	faves := []*gtsmodel.StatusFave{}
 | |
| 	if err := ps.conn.Model(&faves).Where("status_id = ?", status.ID).Select(); err != nil {
 | |
| 		if err == pg.ErrNoRows {
 | |
| 			return accounts, nil // no rows just means nobody has faved this status, so that's fine
 | |
| 		}
 | |
| 		return nil, err // an actual error has occurred
 | |
| 	}
 | |
| 
 | |
| 	for _, f := range faves {
 | |
| 		acc := >smodel.Account{}
 | |
| 		if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil {
 | |
| 			if err == pg.ErrNoRows {
 | |
| 				continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it
 | |
| 			}
 | |
| 			return nil, err // an actual error has occurred
 | |
| 		}
 | |
| 		accounts = append(accounts, acc)
 | |
| 	}
 | |
| 	return accounts, nil
 | |
| }
 | |
| 
 | |
| 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) GetStatusesWhereFollowing(accountID string, limit int, offsetStatusID string) ([]*gtsmodel.Status, error) {
 | |
| 	statuses := []*gtsmodel.Status{}
 | |
| 
 | |
| 	q := ps.conn.Model(&statuses)
 | |
| 
 | |
| 	q = q.ColumnExpr("status.*").
 | |
| 		Join("JOIN follows AS f ON f.target_account_id = status.account_id").
 | |
| 		Where("f.account_id = ?", accountID).
 | |
| 		Order("status.created_at DESC")
 | |
| 
 | |
| 	if offsetStatusID != "" {
 | |
| 		s := >smodel.Status{}
 | |
| 		if err := ps.conn.Model(s).Where("id = ?", offsetStatusID).Select(); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		q = q.Where("status.created_at < ?", s.CreatedAt)
 | |
| 	}
 | |
| 
 | |
| 	if limit > 0 {
 | |
| 		q = q.Limit(limit)
 | |
| 	}
 | |
| 
 | |
| 	err := q.Select()
 | |
| 	if err != nil {
 | |
| 		if err != pg.ErrNoRows {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return statuses, nil
 | |
| }
 | |
| 
 | |
| 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)
 | |
| 
 | |
| 	q = q.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")
 | |
| 
 | |
| 	if maxID != "" {
 | |
| 		s := >smodel.Status{}
 | |
| 		if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		q = q.Where("status.created_at < ?", s.CreatedAt)
 | |
| 	}
 | |
| 
 | |
| 	if minID != "" {
 | |
| 		s := >smodel.Status{}
 | |
| 		if err := ps.conn.Model(s).Where("id = ?", minID).Select(); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		q = q.Where("status.created_at > ?", s.CreatedAt)
 | |
| 	}
 | |
| 
 | |
| 	if sinceID != "" {
 | |
| 		s := >smodel.Status{}
 | |
| 		if err := ps.conn.Model(s).Where("id = ?", sinceID).Select(); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		q = q.Where("status.created_at > ?", s.CreatedAt)
 | |
| 	}
 | |
| 
 | |
| 	err := q.Select()
 | |
| 	if err != nil {
 | |
| 		if err != pg.ErrNoRows {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return statuses, nil
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) GetPublicTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) {
 | |
| 	statuses := []*gtsmodel.Status{}
 | |
| 
 | |
| 	q := ps.conn.Model(&statuses).
 | |
| 		Where("visibility = ?", gtsmodel.VisibilityPublic).
 | |
| 		Limit(limit).
 | |
| 		Order("created_at DESC")
 | |
| 
 | |
| 	if maxID != "" {
 | |
| 		s := >smodel.Status{}
 | |
| 		if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		q = q.Where("created_at < ?", s.CreatedAt)
 | |
| 	}
 | |
| 
 | |
| 	if minID != "" {
 | |
| 		s := >smodel.Status{}
 | |
| 		if err := ps.conn.Model(s).Where("id = ?", minID).Select(); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		q = q.Where("created_at > ?", s.CreatedAt)
 | |
| 	}
 | |
| 
 | |
| 	if sinceID != "" {
 | |
| 		s := >smodel.Status{}
 | |
| 		if err := ps.conn.Model(s).Where("id = ?", sinceID).Select(); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		q = q.Where("created_at > ?", s.CreatedAt)
 | |
| 	}
 | |
| 
 | |
| 	if local {
 | |
| 		q = q.Where("local = ?", local)
 | |
| 	}
 | |
| 
 | |
| 	err := q.Select()
 | |
| 	if err != nil {
 | |
| 		if err != pg.ErrNoRows {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return statuses, nil
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) GetNotificationsForAccount(accountID string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, error) {
 | |
| 	notifications := []*gtsmodel.Notification{}
 | |
| 
 | |
| 	q := ps.conn.Model(¬ifications).Where("target_account_id = ?", accountID)
 | |
| 
 | |
| 	if maxID != "" {
 | |
| 		n := >smodel.Notification{}
 | |
| 		if err := ps.conn.Model(n).Where("id = ?", maxID).Select(); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		q = q.Where("created_at < ?", n.CreatedAt)
 | |
| 	}
 | |
| 
 | |
| 	if sinceID != "" {
 | |
| 		n := >smodel.Notification{}
 | |
| 		if err := ps.conn.Model(n).Where("id = ?", sinceID).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
 | |
| }
 | |
| 
 | |
| /*
 | |
| 	CONVERSION FUNCTIONS
 | |
| */
 | |
| 
 | |
| func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) {
 | |
| 	ogAccount := >smodel.Account{}
 | |
| 	if err := ps.conn.Model(ogAccount).Where("id = ?", originAccountID).Select(); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	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)
 | |
| 		}
 | |
| 
 | |
| 		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)
 | |
| 		}
 | |
| 
 | |
| 		// okay we're good now, we can start pulling accounts out of the database
 | |
| 		mentionedAccount := >smodel.Account{}
 | |
| 		var err error
 | |
| 		if local {
 | |
| 			// local user -- should have a null domain
 | |
| 			err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select()
 | |
| 		} else {
 | |
| 			// remote user -- should have domain defined
 | |
| 			err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? = ?", pg.Ident("domain"), domain).Select()
 | |
| 		}
 | |
| 
 | |
| 		if err != nil {
 | |
| 			if err == pg.ErrNoRows {
 | |
| 				// no result found for this username/domain so just don't include it as a mencho and carry on about our business
 | |
| 				ps.log.Debugf("no account found with username '%s' and domain '%s', skipping it", username, domain)
 | |
| 				continue
 | |
| 			}
 | |
| 			// a serious error has happened so bail
 | |
| 			return nil, fmt.Errorf("error getting account with username '%s' and domain '%s': %s", username, domain, err)
 | |
| 		}
 | |
| 
 | |
| 		// id, createdAt and updatedAt will be populated by the db, so we have everything we need!
 | |
| 		menchies = append(menchies, >smodel.Mention{
 | |
| 			StatusID:            statusID,
 | |
| 			OriginAccountID:     ogAccount.ID,
 | |
| 			OriginAccountURI:    ogAccount.URI,
 | |
| 			TargetAccountID:     mentionedAccount.ID,
 | |
| 			NameString:          a,
 | |
| 			MentionedAccountURI: mentionedAccount.URI,
 | |
| 			GTSAccount:          mentionedAccount,
 | |
| 		})
 | |
| 	}
 | |
| 	return menchies, nil
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) TagStringsToTags(tags []string, originAccountID string, statusID string) ([]*gtsmodel.Tag, error) {
 | |
| 	newTags := []*gtsmodel.Tag{}
 | |
| 	for _, t := range tags {
 | |
| 		tag := >smodel.Tag{}
 | |
| 		// we can use selectorinsert here to create the new tag if it doesn't exist already
 | |
| 		// inserted will be true if this is a new tag we just created
 | |
| 		if err := ps.conn.Model(tag).Where("name = ?", t).Select(); err != nil {
 | |
| 			if err == pg.ErrNoRows {
 | |
| 				// tag doesn't exist yet so populate it
 | |
| 				tag.ID = uuid.NewString()
 | |
| 				tag.Name = t
 | |
| 				tag.FirstSeenFromAccountID = originAccountID
 | |
| 				tag.CreatedAt = time.Now()
 | |
| 				tag.UpdatedAt = time.Now()
 | |
| 				tag.Useable = true
 | |
| 				tag.Listable = true
 | |
| 			} else {
 | |
| 				return nil, fmt.Errorf("error getting tag with name %s: %s", t, err)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// bail already if the tag isn't useable
 | |
| 		if !tag.Useable {
 | |
| 			continue
 | |
| 		}
 | |
| 		tag.LastStatusAt = time.Now()
 | |
| 		newTags = append(newTags, tag)
 | |
| 	}
 | |
| 	return newTags, nil
 | |
| }
 | |
| 
 | |
| func (ps *postgresService) EmojiStringsToEmojis(emojis []string, originAccountID string, statusID string) ([]*gtsmodel.Emoji, error) {
 | |
| 	newEmojis := []*gtsmodel.Emoji{}
 | |
| 	for _, e := range emojis {
 | |
| 		emoji := >smodel.Emoji{}
 | |
| 		err := ps.conn.Model(emoji).Where("shortcode = ?", e).Where("visible_in_picker = true").Where("disabled = false").Select()
 | |
| 		if err != nil {
 | |
| 			if err == pg.ErrNoRows {
 | |
| 				// no result found for this username/domain so just don't include it as an emoji and carry on about our business
 | |
| 				ps.log.Debugf("no emoji found with shortcode %s, skipping it", e)
 | |
| 				continue
 | |
| 			}
 | |
| 			// a serious error has happened so bail
 | |
| 			return nil, fmt.Errorf("error getting emoji with shortcode %s: %s", e, err)
 | |
| 		}
 | |
| 		newEmojis = append(newEmojis, emoji)
 | |
| 	}
 | |
| 	return newEmojis, nil
 | |
| }
 |