/* 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 . */ package db import ( "context" "crypto/rand" "crypto/rsa" "errors" "fmt" "net" "net/mail" "regexp" "strings" "time" "github.com/go-fed/activity/pub" "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/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.Entry 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.Entry) (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.Logger.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, } federatingDB := newFederatingDB(ps, c) ps.federationDB = federatingDB // 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) != dbTypePostgres { return nil, fmt.Errorf("expected db type of %s but got %s", 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 } /* FEDERATION FUNCTIONALITY */ func (ps *postgresService) Federation() pub.Database { return ps.federationDB } /* 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 ErrNoEntries{} } return err } return nil } func (ps *postgresService) GetWhere(key string, value interface{}, i interface{}) error { if err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Select(); err != nil { if err == pg.ErrNoRows { return 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 ErrNoEntries{} } return err } return nil } func (ps *postgresService) Put(i interface{}) error { _, err := ps.conn.Model(i).Insert(i) return err } func (ps *postgresService) Upsert(i interface{}, conflictColumn string) error { if _, err := ps.conn.Model(i).OnConflict(fmt.Sprintf("(%s) DO UPDATE", conflictColumn)).Insert(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} } return err } return nil } func (ps *postgresService) UpdateByID(id string, i interface{}) error { if _, err := ps.conn.Model(i).Where("id = ?", id).OnConflict("(id) DO UPDATE").Insert(); err != nil { if err == pg.ErrNoRows { return 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 err == pg.ErrNoRows { return ErrNoEntries{} } return err } return nil } func (ps *postgresService) DeleteWhere(key string, value interface{}, i interface{}) error { if _, err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Delete(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} } return err } return nil } /* HANDY SHORTCUTS */ func (ps *postgresService) CreateInstanceAccount() error { username := ps.config.Host instanceAccount := >smodel.Account{ Username: username, } inserted, err := ps.conn.Model(instanceAccount).Where("username = ?", username).SelectOrInsert() if err != nil { return err } if inserted { ps.log.Infof("created instance account %s with id %s", username, instanceAccount.ID) } else { ps.log.Infof("instance account %s already exists with id %s", username, instanceAccount.ID) } return nil } func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.Account) error { user := >smodel.User{ ID: userID, } if err := ps.conn.Model(user).Where("id = ?", userID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} } return err } if err := ps.conn.Model(account).Where("id = ?", user.AccountID).Select(); err != nil { if err == pg.ErrNoRows { return 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 ErrNoEntries{} } 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 ErrNoEntries{} } return err } return nil } func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error { if err := ps.conn.Model(followers).Where("target_account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} } return err } return nil } func (ps *postgresService) GetStatusesByAccountID(accountID string, statuses *[]gtsmodel.Status) error { if err := ps.conn.Model(statuses).Where("account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} } return err } return nil } func (ps *postgresService) GetStatusesByTimeDescending(accountID string, statuses *[]gtsmodel.Status, limit int) error { q := ps.conn.Model(statuses).Order("created_at DESC") if limit != 0 { q = q.Limit(limit) } if accountID != "" { q = q.Where("account_id = ?", accountID) } if err := q.Select(); err != nil { if err == pg.ErrNoRows { return 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 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 } uris := util.GenerateURIs(username, ps.config.Protocol, ps.config.Host) a := >smodel.Account{ Username: username, DisplayName: username, Reason: reason, URL: uris.UserURL, PrivateKey: key, PublicKey: &key.PublicKey, ActorType: gtsmodel.ActivityStreamsPerson, URI: uris.UserURI, InboxURL: uris.InboxURI, OutboxURL: uris.OutboxURI, FollowersURL: uris.FollowersURI, FeaturedCollectionURL: uris.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 ErrNoEntries{} } return err } if acct.HeaderMediaAttachmentID == "" { return ErrNoEntries{} } if err := ps.conn.Model(header).Where("id = ?", acct.HeaderMediaAttachmentID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} } 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 ErrNoEntries{} } return err } if acct.AvatarMediaAttachmentID == "" { return ErrNoEntries{} } if err := ps.conn.Model(avatar).Where("id = ?", acct.AvatarMediaAttachmentID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} } return err } return nil } func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) { var blocked bool if err := ps.conn.Model(>smodel.Block{}). Where("account_id = ?", account1).Where("target_account_id = ?", account2). WhereOr("target_account_id = ?", account1).Where("account_id = ?", account2). Select(); err != nil { if err == pg.ErrNoRows { blocked = false return blocked, nil } else { return blocked, err } } blocked = true return blocked, nil } func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) { l := ps.log.WithField("func", "StatusVisible") // if target account is suspended then don't show the status if !targetAccount.SuspendedAt.IsZero() { l.Debug("target account suspended at is not zero") return false, nil } // if the target user doesn't exist (anymore) then the status also shouldn't be visible targetUser := >smodel.User{} if err := ps.conn.Model(targetUser).Where("account_id = ?", targetAccount.ID).Select(); err != nil { l.Debug("target user could not be selected") if err == pg.ErrNoRows { return false, ErrNoEntries{} } else { return false, err } } // if target user is disabled, not yet approved, or not confirmed then don't show the status // (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!) if targetUser.Disabled || !targetUser.Approved || targetUser.ConfirmedAt.IsZero() { l.Debug("target user is disabled, not approved, or not confirmed") return false, nil } // If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed. // In this case, we can still serve the status if it's public, otherwise we definitely shouldn't. if requestingAccount == nil { if targetStatus.Visibility == gtsmodel.VisibilityPublic { return true, nil } l.Debug("requesting account is nil but the target status isn't public") return false, nil } // if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten // this far (ie., been authed) in the first place: this is just for safety. if !requestingAccount.SuspendedAt.IsZero() { l.Debug("requesting account is suspended") return false, nil } // check if we have a local account -- if so we can check the user for that account in the DB if requestingAccount.Domain == "" { requestingUser := >smodel.User{} if err := ps.conn.Model(requestingUser).Where("account_id = ?", requestingAccount.ID).Select(); err != nil { // if the requesting account is local but doesn't have a corresponding user in the db this is a problem if err == pg.ErrNoRows { l.Debug("requesting account is local but there's no corresponding user") return false, nil } else { l.Debugf("requesting account is local but there was an error getting the corresponding user: %s", err) return false, err } } // okay, user exists, so make sure it has full privileges/is confirmed/approved if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() { l.Debug("requesting account is local but corresponding user is either disabled, not approved, or not confirmed") return false, nil } } // if the target status belongs to the requesting account, they should always be able to view it at this point if targetStatus.AccountID == requestingAccount.ID { return true, nil } // At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou // First check if a block exists directly between the target account (which authored the status) and the requesting account. if blocked, err := ps.Blocked(targetAccount.ID, requestingAccount.ID); err != nil { l.Debugf("something went wrong figuring out if the accounts have a block: %s", err) return false, err } else if blocked { // don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please l.Debug("a block exists between requesting account and target account") return false, nil } // check other accounts mentioned/boosted by/replied to by the status, if they exist if relevantAccounts != nil { // status replies to account id if relevantAccounts.ReplyToAccount != nil { if blocked, err := ps.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil { return false, err } else if blocked { return false, nil } } // status boosts accounts id if relevantAccounts.BoostedAccount != nil { if blocked, err := ps.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil { return false, err } else if blocked { return false, nil } } // status boosts a reply to account id if relevantAccounts.BoostedReplyToAccount != nil { if blocked, err := ps.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil { return false, err } else if blocked { return false, nil } } // status mentions accounts for _, a := range relevantAccounts.MentionedAccounts { if blocked, err := ps.Blocked(a.ID, requestingAccount.ID); err != nil { return false, err } else if blocked { return false, nil } } } // at this point we know neither account blocks the other, or another account mentioned or otherwise referred to in the status // that means it's now just a matter of checking the visibility settings of the status itself switch targetStatus.Visibility { case gtsmodel.VisibilityPublic, gtsmodel.VisibilityUnlocked: // no problem here, just return OK return true, nil case gtsmodel.VisibilityFollowersOnly: // check one-way follow follows, err := ps.Follows(requestingAccount, targetAccount) if err != nil { return false, err } if !follows { return false, nil } return true, nil case gtsmodel.VisibilityMutualsOnly: // check mutual follow mutuals, err := ps.Mutuals(requestingAccount, targetAccount) if err != nil { return false, err } if !mutuals { return false, nil } return true, nil case gtsmodel.VisibilityDirect: // make sure the requesting account is mentioned in the status for _, menchie := range targetStatus.Mentions { if menchie == requestingAccount.ID { return true, nil // yep it's mentioned! } } return false, nil // it's not mentioned -_- } return false, errors.New("reached the end of StatusVisible with no result") } func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) { return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() } func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) { // make sure account 1 follows account 2 f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists() if err != nil { if err == pg.ErrNoRows { return false, nil } else { return false, err } } // make sure account 2 follows account 1 f2, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account2.ID).Where("target_account_id = ?", account1.ID).Exists() if err != nil { if err == pg.ErrNoRows { return false, nil } else { return false, err } } return f1 && f2, nil } func (ps *postgresService) PullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error) { accounts := >smodel.RelevantAccounts{ MentionedAccounts: []*gtsmodel.Account{}, } // get the replied to account from the status and add it to the pile if targetStatus.InReplyToAccountID != "" { repliedToAccount := >smodel.Account{} if err := ps.conn.Model(repliedToAccount).Where("id = ?", targetStatus.InReplyToAccountID).Select(); err != nil { return accounts, err } accounts.ReplyToAccount = repliedToAccount } // get the boosted account from the status and add it to the pile if targetStatus.BoostOfID != "" { // retrieve the boosted status first boostedStatus := >smodel.Status{} if err := ps.conn.Model(boostedStatus).Where("id = ?", targetStatus.BoostOfID).Select(); err != nil { return accounts, err } boostedAccount := >smodel.Account{} if err := ps.conn.Model(boostedAccount).Where("id = ?", boostedStatus.AccountID).Select(); err != nil { return accounts, err } accounts.BoostedAccount = boostedAccount // the boosted status might be a reply to another account so we should get that too if boostedStatus.InReplyToAccountID != "" { boostedStatusRepliedToAccount := >smodel.Account{} if err := ps.conn.Model(boostedStatusRepliedToAccount).Where("id = ?", boostedStatus.InReplyToAccountID).Select(); err != nil { return accounts, err } accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount } } // now get all accounts with IDs that are mentioned in the status for _, mentionedAccountID := range targetStatus.Mentions { mentionedAccount := >smodel.Account{} if err := ps.conn.Model(mentionedAccount).Where("id = ?", mentionedAccountID).Select(); err != nil { return accounts, err } accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount) } return accounts, nil } func (ps *postgresService) GetReplyCountForStatus(status *gtsmodel.Status) (int, error) { return ps.conn.Model(>smodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count() } func (ps *postgresService) GetReblogCountForStatus(status *gtsmodel.Status) (int, error) { return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Count() } func (ps *postgresService) GetFaveCountForStatus(status *gtsmodel.Status) (int, error) { return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Count() } func (ps *postgresService) StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) { return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() } func (ps *postgresService) StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) { return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists() } func (ps *postgresService) StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) { return ps.conn.Model(>smodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() } func (ps *postgresService) StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) { return ps.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() } func (ps *postgresService) StatusPinnedBy(status *gtsmodel.Status, accountID string) (bool, error) { return ps.conn.Model(>smodel.StatusPin{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() } func (ps *postgresService) FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { // first check if a fave already exists, we can just return if so existingFave := >smodel.StatusFave{} err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() if err == nil { // fave already exists so just return nothing at all return nil, nil } // an error occurred so it might exist or not, we don't know if err != pg.ErrNoRows { return nil, err } // it doesn't exist so create it newFave := >smodel.StatusFave{ AccountID: accountID, TargetAccountID: status.AccountID, StatusID: status.ID, } if _, err = ps.conn.Model(newFave).Insert(); err != nil { return nil, err } return newFave, nil } func (ps *postgresService) UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { // if a fave doesn't exist, we don't need to do anything existingFave := >smodel.StatusFave{} err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() // the fave doesn't exist so return nothing at all if err == pg.ErrNoRows { return nil, nil } // an error occurred so it might exist or not, we don't know if err != nil && err != pg.ErrNoRows { return nil, err } // the fave exists so remove it if _, err = ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Delete(); err != nil { return nil, err } return existingFave, nil } func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) { accounts := []*gtsmodel.Account{} faves := []*gtsmodel.StatusFave{} if err := ps.conn.Model(&faves).Where("status_id = ?", status.ID).Select(); err != nil { if err == pg.ErrNoRows { return accounts, nil // no rows just means nobody has faved this status, so that's fine } return nil, err // an actual error has occurred } for _, f := range faves { acc := >smodel.Account{} if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil { if err == pg.ErrNoRows { continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it } return nil, err // an actual error has occurred } accounts = append(accounts, acc) } return accounts, nil } /* CONVERSION FUNCTIONS */ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) { menchies := []*gtsmodel.Mention{} for _, a := range targetAccounts { // A mentioned account looks like "@test@example.org" or just "@test" for a local account // -- we can guarantee this from the regex that targetAccounts should have been derived from. // But we still need to do a bit of fiddling to get what we need here -- the username and domain (if given). // 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: originAccountID, TargetAccountID: mentionedAccount.ID, }) } 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 }