summaryrefslogtreecommitdiff
path: root/internal/apimodule
diff options
context:
space:
mode:
Diffstat (limited to 'internal/apimodule')
-rw-r--r--internal/apimodule/account/account.go117
-rw-r--r--internal/apimodule/account/accountcreate.go155
-rw-r--r--internal/apimodule/account/accountget.go57
-rw-r--r--internal/apimodule/account/accountupdate.go260
-rw-r--r--internal/apimodule/account/accountverify.go50
-rw-r--r--internal/apimodule/account/test/accountcreate_test.go551
-rw-r--r--internal/apimodule/account/test/accountupdate_test.go303
-rw-r--r--internal/apimodule/account/test/accountverify_test.go19
-rw-r--r--internal/apimodule/admin/admin.go88
-rw-r--r--internal/apimodule/admin/emojicreate.go130
-rw-r--r--internal/apimodule/apimodule.go33
-rw-r--r--internal/apimodule/app/app.go77
-rw-r--r--internal/apimodule/app/appcreate.go119
-rw-r--r--internal/apimodule/app/test/app_test.go21
-rw-r--r--internal/apimodule/auth/auth.go88
-rw-r--r--internal/apimodule/auth/authorize.go204
-rw-r--r--internal/apimodule/auth/middleware.go76
-rw-r--r--internal/apimodule/auth/signin.go116
-rw-r--r--internal/apimodule/auth/test/auth_test.go166
-rw-r--r--internal/apimodule/auth/token.go36
-rw-r--r--internal/apimodule/fileserver/fileserver.go84
-rw-r--r--internal/apimodule/fileserver/servefile.go243
-rw-r--r--internal/apimodule/fileserver/test/servefile_test.go157
-rw-r--r--internal/apimodule/media/media.go76
-rw-r--r--internal/apimodule/media/mediacreate.go193
-rw-r--r--internal/apimodule/media/test/mediacreate_test.go194
-rw-r--r--internal/apimodule/mock_ClientAPIModule.go43
-rw-r--r--internal/apimodule/security/flocblock.go28
-rw-r--r--internal/apimodule/security/security.go52
-rw-r--r--internal/apimodule/status/status.go158
-rw-r--r--internal/apimodule/status/statuscreate.go462
-rw-r--r--internal/apimodule/status/statusdelete.go107
-rw-r--r--internal/apimodule/status/statusfave.go137
-rw-r--r--internal/apimodule/status/statusfavedby.go129
-rw-r--r--internal/apimodule/status/statusget.go112
-rw-r--r--internal/apimodule/status/statusunfave.go137
-rw-r--r--internal/apimodule/status/test/statuscreate_test.go346
-rw-r--r--internal/apimodule/status/test/statusfave_test.go207
-rw-r--r--internal/apimodule/status/test/statusfavedby_test.go159
-rw-r--r--internal/apimodule/status/test/statusget_test.go168
-rw-r--r--internal/apimodule/status/test/statusunfave_test.go219
41 files changed, 0 insertions, 6077 deletions
diff --git a/internal/apimodule/account/account.go b/internal/apimodule/account/account.go
deleted file mode 100644
index a836afcdb..000000000
--- a/internal/apimodule/account/account.go
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package account
-
-import (
- "fmt"
- "net/http"
- "strings"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
-
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/router"
-)
-
-const (
- // IDKey is the key to use for retrieving account ID in requests
- IDKey = "id"
- // BasePath is the base API path for this module
- BasePath = "/api/v1/accounts"
- // BasePathWithID is the base path for this module with the ID key
- BasePathWithID = BasePath + "/:" + IDKey
- // VerifyPath is for verifying account credentials
- VerifyPath = BasePath + "/verify_credentials"
- // UpdateCredentialsPath is for updating account credentials
- UpdateCredentialsPath = BasePath + "/update_credentials"
-)
-
-// Module implements the ClientAPIModule interface for account-related actions
-type Module struct {
- config *config.Config
- db db.DB
- oauthServer oauth.Server
- mediaHandler media.Handler
- mastoConverter mastotypes.Converter
- log *logrus.Logger
-}
-
-// New returns a new account module
-func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler media.Handler, mastoConverter mastotypes.Converter, log *logrus.Logger) apimodule.ClientAPIModule {
- return &Module{
- config: config,
- db: db,
- oauthServer: oauthServer,
- mediaHandler: mediaHandler,
- mastoConverter: mastoConverter,
- log: log,
- }
-}
-
-// Route attaches all routes from this module to the given router
-func (m *Module) Route(r router.Router) error {
- r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler)
- r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
- r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler)
- return nil
-}
-
-// CreateTables creates the required tables for this module in the given database
-func (m *Module) CreateTables(db db.DB) error {
- models := []interface{}{
- &gtsmodel.User{},
- &gtsmodel.Account{},
- &gtsmodel.Follow{},
- &gtsmodel.FollowRequest{},
- &gtsmodel.Status{},
- &gtsmodel.Application{},
- &gtsmodel.EmailDomainBlock{},
- &gtsmodel.MediaAttachment{},
- }
-
- for _, m := range models {
- if err := db.CreateTable(m); err != nil {
- return fmt.Errorf("error creating table: %s", err)
- }
- }
- return nil
-}
-
-func (m *Module) muxHandler(c *gin.Context) {
- ru := c.Request.RequestURI
- switch c.Request.Method {
- case http.MethodGet:
- if strings.HasPrefix(ru, VerifyPath) {
- m.AccountVerifyGETHandler(c)
- } else {
- m.AccountGETHandler(c)
- }
- case http.MethodPatch:
- if strings.HasPrefix(ru, UpdateCredentialsPath) {
- m.AccountUpdateCredentialsPATCHHandler(c)
- }
- }
-}
diff --git a/internal/apimodule/account/accountcreate.go b/internal/apimodule/account/accountcreate.go
deleted file mode 100644
index fb21925b8..000000000
--- a/internal/apimodule/account/accountcreate.go
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package account
-
-import (
- "errors"
- "fmt"
- "net"
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/util"
- "github.com/superseriousbusiness/oauth2/v4"
-)
-
-// AccountCreatePOSTHandler handles create account requests, validates them,
-// and puts them in the database if they're valid.
-// It should be served as a POST at /api/v1/accounts
-func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
- l := m.log.WithField("func", "accountCreatePOSTHandler")
- authed, err := oauth.MustAuth(c, true, true, false, false)
- if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
- return
- }
-
- l.Trace("parsing request form")
- form := &mastotypes.AccountCreateRequest{}
- if err := c.ShouldBind(form); err != nil || form == nil {
- l.Debugf("could not parse form from request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
- return
- }
-
- l.Tracef("validating form %+v", form)
- if err := validateCreateAccount(form, m.config.AccountsConfig, m.db); err != nil {
- l.Debugf("error validating form: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- clientIP := c.ClientIP()
- l.Tracef("attempting to parse client ip address %s", clientIP)
- signUpIP := net.ParseIP(clientIP)
- if signUpIP == nil {
- l.Debugf("error validating sign up ip address %s", clientIP)
- c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"})
- return
- }
-
- ti, err := m.accountCreate(form, signUpIP, authed.Token, authed.Application)
- if err != nil {
- l.Errorf("internal server error while creating new account: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- c.JSON(http.StatusOK, ti)
-}
-
-// accountCreate does the dirty work of making an account and user in the database.
-// It then returns a token to the caller, for use with the new account, as per the
-// spec here: https://docs.joinmastodon.org/methods/accounts/
-func (m *Module) accountCreate(form *mastotypes.AccountCreateRequest, signUpIP net.IP, token oauth2.TokenInfo, app *gtsmodel.Application) (*mastotypes.Token, error) {
- l := m.log.WithField("func", "accountCreate")
-
- // don't store a reason if we don't require one
- reason := form.Reason
- if !m.config.AccountsConfig.ReasonRequired {
- reason = ""
- }
-
- l.Trace("creating new username and account")
- user, err := m.db.NewSignup(form.Username, reason, m.config.AccountsConfig.RequireApproval, form.Email, form.Password, signUpIP, form.Locale, app.ID)
- if err != nil {
- return nil, fmt.Errorf("error creating new signup in the database: %s", err)
- }
-
- l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, app.ID)
- accessToken, err := m.oauthServer.GenerateUserAccessToken(token, app.ClientSecret, user.ID)
- if err != nil {
- return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)
- }
-
- return &mastotypes.Token{
- AccessToken: accessToken.GetAccess(),
- TokenType: "Bearer",
- Scope: accessToken.GetScope(),
- CreatedAt: accessToken.GetAccessCreateAt().Unix(),
- }, nil
-}
-
-// validateCreateAccount checks through all the necessary prerequisites for creating a new account,
-// according to the provided account create request. If the account isn't eligible, an error will be returned.
-func validateCreateAccount(form *mastotypes.AccountCreateRequest, c *config.AccountsConfig, database db.DB) error {
- if !c.OpenRegistration {
- return errors.New("registration is not open for this server")
- }
-
- if err := util.ValidateUsername(form.Username); err != nil {
- return err
- }
-
- if err := util.ValidateEmail(form.Email); err != nil {
- return err
- }
-
- if err := util.ValidateNewPassword(form.Password); err != nil {
- return err
- }
-
- if !form.Agreement {
- return errors.New("agreement to terms and conditions not given")
- }
-
- if err := util.ValidateLanguage(form.Locale); err != nil {
- return err
- }
-
- if err := util.ValidateSignUpReason(form.Reason, c.ReasonRequired); err != nil {
- return err
- }
-
- if err := database.IsEmailAvailable(form.Email); err != nil {
- return err
- }
-
- if err := database.IsUsernameAvailable(form.Username); err != nil {
- return err
- }
-
- return nil
-}
diff --git a/internal/apimodule/account/accountget.go b/internal/apimodule/account/accountget.go
deleted file mode 100644
index 5003be139..000000000
--- a/internal/apimodule/account/accountget.go
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package account
-
-import (
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
-)
-
-// AccountGETHandler serves the account information held by the server in response to a GET
-// request. It should be served as a GET at /api/v1/accounts/:id.
-//
-// See: https://docs.joinmastodon.org/methods/accounts/
-func (m *Module) AccountGETHandler(c *gin.Context) {
- targetAcctID := c.Param(IDKey)
- if targetAcctID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
- return
- }
-
- targetAccount := &gtsmodel.Account{}
- if err := m.db.GetByID(targetAcctID, targetAccount); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"})
- return
- }
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- acctInfo, err := m.mastoConverter.AccountToMastoPublic(targetAccount)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- c.JSON(http.StatusOK, acctInfo)
-}
diff --git a/internal/apimodule/account/accountupdate.go b/internal/apimodule/account/accountupdate.go
deleted file mode 100644
index 7709697bf..000000000
--- a/internal/apimodule/account/accountupdate.go
+++ /dev/null
@@ -1,260 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package account
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "mime/multipart"
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-// AccountUpdateCredentialsPATCHHandler allows a user to modify their account/profile settings.
-// It should be served as a PATCH at /api/v1/accounts/update_credentials
-//
-// TODO: this can be optimized massively by building up a picture of what we want the new account
-// details to be, and then inserting it all in the database at once. As it is, we do queries one-by-one
-// which is not gonna make the database very happy when lots of requests are going through.
-// This way it would also be safer because the update won't happen until *all* the fields are validated.
-// Otherwise we risk doing a partial update and that's gonna cause probllleeemmmsss.
-func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) {
- l := m.log.WithField("func", "accountUpdateCredentialsPATCHHandler")
- authed, err := oauth.MustAuth(c, true, false, false, true)
- if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
- return
- }
- l.Tracef("retrieved account %+v", authed.Account.ID)
-
- l.Trace("parsing request form")
- form := &mastotypes.UpdateCredentialsRequest{}
- if err := c.ShouldBind(form); err != nil || form == nil {
- l.Debugf("could not parse form from request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- // if everything on the form is nil, then nothing has been set and we shouldn't continue
- if form.Discoverable == nil && form.Bot == nil && form.DisplayName == nil && form.Note == nil && form.Avatar == nil && form.Header == nil && form.Locked == nil && form.Source == nil && form.FieldsAttributes == nil {
- l.Debugf("could not parse form from request")
- c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"})
- return
- }
-
- if form.Discoverable != nil {
- if err := m.db.UpdateOneByID(authed.Account.ID, "discoverable", *form.Discoverable, &gtsmodel.Account{}); err != nil {
- l.Debugf("error updating discoverable: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- }
-
- if form.Bot != nil {
- if err := m.db.UpdateOneByID(authed.Account.ID, "bot", *form.Bot, &gtsmodel.Account{}); err != nil {
- l.Debugf("error updating bot: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- }
-
- if form.DisplayName != nil {
- if err := util.ValidateDisplayName(*form.DisplayName); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- if err := m.db.UpdateOneByID(authed.Account.ID, "display_name", *form.DisplayName, &gtsmodel.Account{}); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- }
-
- if form.Note != nil {
- if err := util.ValidateNote(*form.Note); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- if err := m.db.UpdateOneByID(authed.Account.ID, "note", *form.Note, &gtsmodel.Account{}); err != nil {
- l.Debugf("error updating note: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- }
-
- if form.Avatar != nil && form.Avatar.Size != 0 {
- avatarInfo, err := m.UpdateAccountAvatar(form.Avatar, authed.Account.ID)
- if err != nil {
- l.Debugf("could not update avatar for account %s: %s", authed.Account.ID, err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- l.Tracef("new avatar info for account %s is %+v", authed.Account.ID, avatarInfo)
- }
-
- if form.Header != nil && form.Header.Size != 0 {
- headerInfo, err := m.UpdateAccountHeader(form.Header, authed.Account.ID)
- if err != nil {
- l.Debugf("could not update header for account %s: %s", authed.Account.ID, err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- l.Tracef("new header info for account %s is %+v", authed.Account.ID, headerInfo)
- }
-
- if form.Locked != nil {
- if err := m.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, &gtsmodel.Account{}); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- }
-
- if form.Source != nil {
- if form.Source.Language != nil {
- if err := util.ValidateLanguage(*form.Source.Language); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- if err := m.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, &gtsmodel.Account{}); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- }
-
- if form.Source.Sensitive != nil {
- if err := m.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, &gtsmodel.Account{}); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- }
-
- if form.Source.Privacy != nil {
- if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- if err := m.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, &gtsmodel.Account{}); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- }
- }
-
- // if form.FieldsAttributes != nil {
- // // TODO: parse fields attributes nicely and update
- // }
-
- // fetch the account with all updated values set
- updatedAccount := &gtsmodel.Account{}
- if err := m.db.GetByID(authed.Account.ID, updatedAccount); err != nil {
- l.Debugf("could not fetch updated account %s: %s", authed.Account.ID, err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- acctSensitive, err := m.mastoConverter.AccountToMastoSensitive(updatedAccount)
- if err != nil {
- l.Tracef("could not convert account into mastosensitive account: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- l.Tracef("conversion successful, returning OK and mastosensitive account %+v", acctSensitive)
- c.JSON(http.StatusOK, acctSensitive)
-}
-
-/*
- HELPER FUNCTIONS
-*/
-
-// TODO: try to combine the below two functions because this is a lot of code repetition.
-
-// UpdateAccountAvatar does the dirty work of checking the avatar part of an account update form,
-// parsing and checking the image, and doing the necessary updates in the database for this to become
-// the account's new avatar image.
-func (m *Module) UpdateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
- var err error
- if int(avatar.Size) > m.config.MediaConfig.MaxImageSize {
- err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, m.config.MediaConfig.MaxImageSize)
- return nil, err
- }
- f, err := avatar.Open()
- if err != nil {
- return nil, fmt.Errorf("could not read provided avatar: %s", err)
- }
-
- // extract the bytes
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, f)
- if err != nil {
- return nil, fmt.Errorf("could not read provided avatar: %s", err)
- }
- if size == 0 {
- return nil, errors.New("could not read provided avatar: size 0 bytes")
- }
-
- // do the setting
- avatarInfo, err := m.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.MediaAvatar)
- if err != nil {
- return nil, fmt.Errorf("error processing avatar: %s", err)
- }
-
- return avatarInfo, f.Close()
-}
-
-// UpdateAccountHeader does the dirty work of checking the header part of an account update form,
-// parsing and checking the image, and doing the necessary updates in the database for this to become
-// the account's new header image.
-func (m *Module) UpdateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
- var err error
- if int(header.Size) > m.config.MediaConfig.MaxImageSize {
- err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, m.config.MediaConfig.MaxImageSize)
- return nil, err
- }
- f, err := header.Open()
- if err != nil {
- return nil, fmt.Errorf("could not read provided header: %s", err)
- }
-
- // extract the bytes
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, f)
- if err != nil {
- return nil, fmt.Errorf("could not read provided header: %s", err)
- }
- if size == 0 {
- return nil, errors.New("could not read provided header: size 0 bytes")
- }
-
- // do the setting
- headerInfo, err := m.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.MediaHeader)
- if err != nil {
- return nil, fmt.Errorf("error processing header: %s", err)
- }
-
- return headerInfo, f.Close()
-}
diff --git a/internal/apimodule/account/accountverify.go b/internal/apimodule/account/accountverify.go
deleted file mode 100644
index 9edf1e73a..000000000
--- a/internal/apimodule/account/accountverify.go
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package account
-
-import (
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-// AccountVerifyGETHandler serves a user's account details to them IF they reached this
-// handler while in possession of a valid token, according to the oauth middleware.
-// It should be served as a GET at /api/v1/accounts/verify_credentials
-func (m *Module) AccountVerifyGETHandler(c *gin.Context) {
- l := m.log.WithField("func", "accountVerifyGETHandler")
- authed, err := oauth.MustAuth(c, true, false, false, true)
- if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
- return
- }
-
- l.Tracef("retrieved account %+v, converting to mastosensitive...", authed.Account.ID)
- acctSensitive, err := m.mastoConverter.AccountToMastoSensitive(authed.Account)
- if err != nil {
- l.Tracef("could not convert account into mastosensitive account: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- l.Tracef("conversion successful, returning OK and mastosensitive account %+v", acctSensitive)
- c.JSON(http.StatusOK, acctSensitive)
-}
diff --git a/internal/apimodule/account/test/accountcreate_test.go b/internal/apimodule/account/test/accountcreate_test.go
deleted file mode 100644
index 81eab467a..000000000
--- a/internal/apimodule/account/test/accountcreate_test.go
+++ /dev/null
@@ -1,551 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package account
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "io/ioutil"
- "mime/multipart"
- "net/http"
- "net/http/httptest"
- "net/url"
- "os"
- "testing"
- "time"
-
- "github.com/gin-gonic/gin"
- "github.com/google/uuid"
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule/account"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
-
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/oauth2/v4"
- "github.com/superseriousbusiness/oauth2/v4/models"
- oauthmodels "github.com/superseriousbusiness/oauth2/v4/models"
- "golang.org/x/crypto/bcrypt"
-)
-
-type AccountCreateTestSuite struct {
- suite.Suite
- config *config.Config
- log *logrus.Logger
- testAccountLocal *gtsmodel.Account
- testApplication *gtsmodel.Application
- testToken oauth2.TokenInfo
- mockOauthServer *oauth.MockServer
- mockStorage *storage.MockStorage
- mediaHandler media.Handler
- mastoConverter mastotypes.Converter
- db db.DB
- accountModule *account.Module
- newUserFormHappyPath url.Values
-}
-
-/*
- TEST INFRASTRUCTURE
-*/
-
-// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
-func (suite *AccountCreateTestSuite) SetupSuite() {
- // some of our subsequent entities need a log so create this here
- log := logrus.New()
- log.SetLevel(logrus.TraceLevel)
- suite.log = log
-
- suite.testAccountLocal = &gtsmodel.Account{
- ID: uuid.NewString(),
- Username: "test_user",
- }
-
- // can use this test application throughout
- suite.testApplication = &gtsmodel.Application{
- ID: "weeweeeeeeeeeeeeee",
- Name: "a test application",
- Website: "https://some-application-website.com",
- RedirectURI: "http://localhost:8080",
- ClientID: "a-known-client-id",
- ClientSecret: "some-secret",
- Scopes: "read",
- VapidKey: "aaaaaa-aaaaaaaa-aaaaaaaaaaa",
- }
-
- // can use this test token throughout
- suite.testToken = &oauthmodels.Token{
- ClientID: "a-known-client-id",
- RedirectURI: "http://localhost:8080",
- Scope: "read",
- Code: "123456789",
- CodeCreateAt: time.Now(),
- CodeExpiresIn: time.Duration(10 * time.Minute),
- }
-
- // Direct config to local postgres instance
- c := config.Empty()
- c.Protocol = "http"
- c.Host = "localhost"
- c.DBConfig = &config.DBConfig{
- Type: "postgres",
- Address: "localhost",
- Port: 5432,
- User: "postgres",
- Password: "postgres",
- Database: "postgres",
- ApplicationName: "gotosocial",
- }
- c.MediaConfig = &config.MediaConfig{
- MaxImageSize: 2 << 20,
- }
- c.StorageConfig = &config.StorageConfig{
- Backend: "local",
- BasePath: "/tmp",
- ServeProtocol: "http",
- ServeHost: "localhost",
- ServeBasePath: "/fileserver/media",
- }
- suite.config = c
-
- // use an actual database for this, because it's just easier than mocking one out
- database, err := db.New(context.Background(), c, log)
- if err != nil {
- suite.FailNow(err.Error())
- }
- suite.db = database
-
- // we need to mock the oauth server because account creation needs it to create a new token
- suite.mockOauthServer = &oauth.MockServer{}
- suite.mockOauthServer.On("GenerateUserAccessToken", suite.testToken, suite.testApplication.ClientSecret, mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
- l := suite.log.WithField("func", "GenerateUserAccessToken")
- token := args.Get(0).(oauth2.TokenInfo)
- l.Infof("received token %+v", token)
- clientSecret := args.Get(1).(string)
- l.Infof("received clientSecret %+v", clientSecret)
- userID := args.Get(2).(string)
- l.Infof("received userID %+v", userID)
- }).Return(&models.Token{
- Access: "we're authorized now!",
- }, nil)
-
- suite.mockStorage = &storage.MockStorage{}
- // We don't need storage to do anything for these tests, so just simulate a success and do nothing -- we won't need to return anything from storage
- suite.mockStorage.On("StoreFileAt", mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(nil)
-
- // set a media handler because some handlers (eg update credentials) need to upload media (new header/avatar)
- suite.mediaHandler = media.New(suite.config, suite.db, suite.mockStorage, log)
-
- suite.mastoConverter = mastotypes.New(suite.config, suite.db)
-
- // and finally here's the thing we're actually testing!
- suite.accountModule = account.New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.log).(*account.Module)
-}
-
-func (suite *AccountCreateTestSuite) TearDownSuite() {
- if err := suite.db.Stop(context.Background()); err != nil {
- logrus.Panicf("error closing db connection: %s", err)
- }
-}
-
-// SetupTest creates a db connection and creates necessary tables before each test
-func (suite *AccountCreateTestSuite) SetupTest() {
- // create all the tables we might need in thie suite
- models := []interface{}{
- &gtsmodel.User{},
- &gtsmodel.Account{},
- &gtsmodel.Follow{},
- &gtsmodel.FollowRequest{},
- &gtsmodel.Status{},
- &gtsmodel.Application{},
- &gtsmodel.EmailDomainBlock{},
- &gtsmodel.MediaAttachment{},
- }
- for _, m := range models {
- if err := suite.db.CreateTable(m); err != nil {
- logrus.Panicf("db connection error: %s", err)
- }
- }
-
- // form to submit for happy path account create requests -- this will be changed inside tests so it's better to set it before each test
- suite.newUserFormHappyPath = url.Values{
- "reason": []string{"a very good reason that's at least 40 characters i swear"},
- "username": []string{"test_user"},
- "email": []string{"user@example.org"},
- "password": []string{"very-strong-password"},
- "agreement": []string{"true"},
- "locale": []string{"en"},
- }
-
- // same with accounts config
- suite.config.AccountsConfig = &config.AccountsConfig{
- OpenRegistration: true,
- RequireApproval: true,
- ReasonRequired: true,
- }
-}
-
-// TearDownTest drops tables to make sure there's no data in the db
-func (suite *AccountCreateTestSuite) TearDownTest() {
-
- // remove all the tables we might have used so it's clear for the next test
- models := []interface{}{
- &gtsmodel.User{},
- &gtsmodel.Account{},
- &gtsmodel.Follow{},
- &gtsmodel.FollowRequest{},
- &gtsmodel.Status{},
- &gtsmodel.Application{},
- &gtsmodel.EmailDomainBlock{},
- &gtsmodel.MediaAttachment{},
- }
- for _, m := range models {
- if err := suite.db.DropTable(m); err != nil {
- logrus.Panicf("error dropping table: %s", err)
- }
- }
-}
-
-/*
- ACTUAL TESTS
-*/
-
-/*
- TESTING: AccountCreatePOSTHandler
-*/
-
-// TestAccountCreatePOSTHandlerSuccessful checks the happy path for an account creation request: all the fields provided are valid,
-// and at the end of it a new user and account should be added into the database.
-//
-// This is the handler served at /api/v1/accounts as POST
-func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerSuccessful() {
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
- ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = suite.newUserFormHappyPath
- suite.accountModule.AccountCreatePOSTHandler(ctx)
-
- // check response
-
- // 1. we should have OK from our call to the function
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- // 2. we should have a token in the result body
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- t := &mastomodel.Token{}
- err = json.Unmarshal(b, t)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), "we're authorized now!", t.AccessToken)
-
- // check new account
-
- // 1. we should be able to get the new account from the db
- acct := &gtsmodel.Account{}
- err = suite.db.GetWhere("username", "test_user", acct)
- assert.NoError(suite.T(), err)
- assert.NotNil(suite.T(), acct)
- // 2. reason should be set
- assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("reason"), acct.Reason)
- // 3. display name should be equal to username by default
- assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("username"), acct.DisplayName)
- // 4. domain should be nil because this is a local account
- assert.Nil(suite.T(), nil, acct.Domain)
- // 5. id should be set and parseable as a uuid
- assert.NotNil(suite.T(), acct.ID)
- _, err = uuid.Parse(acct.ID)
- assert.Nil(suite.T(), err)
- // 6. private and public key should be set
- assert.NotNil(suite.T(), acct.PrivateKey)
- assert.NotNil(suite.T(), acct.PublicKey)
-
- // check new user
-
- // 1. we should be able to get the new user from the db
- usr := &gtsmodel.User{}
- err = suite.db.GetWhere("unconfirmed_email", suite.newUserFormHappyPath.Get("email"), usr)
- assert.Nil(suite.T(), err)
- assert.NotNil(suite.T(), usr)
-
- // 2. user should have account id set to account we got above
- assert.Equal(suite.T(), acct.ID, usr.AccountID)
-
- // 3. id should be set and parseable as a uuid
- assert.NotNil(suite.T(), usr.ID)
- _, err = uuid.Parse(usr.ID)
- assert.Nil(suite.T(), err)
-
- // 4. locale should be equal to what we requested
- assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("locale"), usr.Locale)
-
- // 5. created by application id should be equal to the app id
- assert.Equal(suite.T(), suite.testApplication.ID, usr.CreatedByApplicationID)
-
- // 6. password should be matcheable to what we set above
- err = bcrypt.CompareHashAndPassword([]byte(usr.EncryptedPassword), []byte(suite.newUserFormHappyPath.Get("password")))
- assert.Nil(suite.T(), err)
-}
-
-// TestAccountCreatePOSTHandlerNoAuth makes sure that the handler fails when no authorization is provided:
-// only registered applications can create accounts, and we don't provide one here.
-func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerNoAuth() {
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = suite.newUserFormHappyPath
- suite.accountModule.AccountCreatePOSTHandler(ctx)
-
- // check response
-
- // 1. we should have forbidden from our call to the function because we didn't auth
- suite.EqualValues(http.StatusForbidden, recorder.Code)
-
- // 2. we should have an error message in the result body
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b))
-}
-
-// TestAccountCreatePOSTHandlerNoAuth makes sure that the handler fails when no form is provided at all.
-func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerNoForm() {
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
- ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
- suite.accountModule.AccountCreatePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusBadRequest, recorder.Code)
-
- // 2. we should have an error message in the result body
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), `{"error":"missing one or more required form values"}`, string(b))
-}
-
-// TestAccountCreatePOSTHandlerWeakPassword makes sure that the handler fails when a weak password is provided
-func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerWeakPassword() {
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
- ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = suite.newUserFormHappyPath
- // set a weak password
- ctx.Request.Form.Set("password", "weak")
- suite.accountModule.AccountCreatePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusBadRequest, recorder.Code)
-
- // 2. we should have an error message in the result body
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), `{"error":"insecure password, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
-}
-
-// TestAccountCreatePOSTHandlerWeirdLocale makes sure that the handler fails when a weird locale is provided
-func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerWeirdLocale() {
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
- ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = suite.newUserFormHappyPath
- // set an invalid locale
- ctx.Request.Form.Set("locale", "neverneverland")
- suite.accountModule.AccountCreatePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusBadRequest, recorder.Code)
-
- // 2. we should have an error message in the result body
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), `{"error":"language: tag is not well-formed"}`, string(b))
-}
-
-// TestAccountCreatePOSTHandlerRegistrationsClosed makes sure that the handler fails when registrations are closed
-func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerRegistrationsClosed() {
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
- ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = suite.newUserFormHappyPath
-
- // close registrations
- suite.config.AccountsConfig.OpenRegistration = false
- suite.accountModule.AccountCreatePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusBadRequest, recorder.Code)
-
- // 2. we should have an error message in the result body
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), `{"error":"registration is not open for this server"}`, string(b))
-}
-
-// TestAccountCreatePOSTHandlerReasonNotProvided makes sure that the handler fails when no reason is provided but one is required
-func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerReasonNotProvided() {
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
- ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = suite.newUserFormHappyPath
-
- // remove reason
- ctx.Request.Form.Set("reason", "")
-
- suite.accountModule.AccountCreatePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusBadRequest, recorder.Code)
-
- // 2. we should have an error message in the result body
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), `{"error":"no reason provided"}`, string(b))
-}
-
-// TestAccountCreatePOSTHandlerReasonNotProvided makes sure that the handler fails when a crappy reason is presented but a good one is required
-func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerInsufficientReason() {
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
- ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = suite.newUserFormHappyPath
-
- // remove reason
- ctx.Request.Form.Set("reason", "just cuz")
-
- suite.accountModule.AccountCreatePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusBadRequest, recorder.Code)
-
- // 2. we should have an error message in the result body
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), `{"error":"reason should be at least 40 chars but 'just cuz' was 8"}`, string(b))
-}
-
-/*
- TESTING: AccountUpdateCredentialsPATCHHandler
-*/
-
-func (suite *AccountCreateTestSuite) TestAccountUpdateCredentialsPATCHHandler() {
-
- // put test local account in db
- err := suite.db.Put(suite.testAccountLocal)
- assert.NoError(suite.T(), err)
-
- // attach avatar to request
- aviFile, err := os.Open("../../media/test/test-jpeg.jpg")
- assert.NoError(suite.T(), err)
- body := &bytes.Buffer{}
- writer := multipart.NewWriter(body)
-
- part, err := writer.CreateFormFile("avatar", "test-jpeg.jpg")
- assert.NoError(suite.T(), err)
-
- _, err = io.Copy(part, aviFile)
- assert.NoError(suite.T(), err)
-
- err = aviFile.Close()
- assert.NoError(suite.T(), err)
-
- err = writer.Close()
- assert.NoError(suite.T(), err)
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccountLocal)
- ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
- ctx.Request = httptest.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:8080/%s", account.UpdateCredentialsPath), body) // the endpoint we're hitting
- ctx.Request.Header.Set("Content-Type", writer.FormDataContentType())
- suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx)
-
- // check response
-
- // 1. we should have OK because our request was valid
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- // 2. we should have an error message in the result body
- result := recorder.Result()
- defer result.Body.Close()
- // TODO: implement proper checks here
- //
- // b, err := ioutil.ReadAll(result.Body)
- // assert.NoError(suite.T(), err)
- // assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b))
-}
-
-func TestAccountCreateTestSuite(t *testing.T) {
- suite.Run(t, new(AccountCreateTestSuite))
-}
diff --git a/internal/apimodule/account/test/accountupdate_test.go b/internal/apimodule/account/test/accountupdate_test.go
deleted file mode 100644
index 1c6f528a1..000000000
--- a/internal/apimodule/account/test/accountupdate_test.go
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package account
-
-import (
- "bytes"
- "context"
- "fmt"
- "io"
- "mime/multipart"
- "net/http"
- "net/http/httptest"
- "net/url"
- "os"
- "testing"
- "time"
-
- "github.com/gin-gonic/gin"
- "github.com/google/uuid"
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule/account"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/oauth2/v4"
- "github.com/superseriousbusiness/oauth2/v4/models"
- oauthmodels "github.com/superseriousbusiness/oauth2/v4/models"
-)
-
-type AccountUpdateTestSuite struct {
- suite.Suite
- config *config.Config
- log *logrus.Logger
- testAccountLocal *gtsmodel.Account
- testApplication *gtsmodel.Application
- testToken oauth2.TokenInfo
- mockOauthServer *oauth.MockServer
- mockStorage *storage.MockStorage
- mediaHandler media.Handler
- mastoConverter mastotypes.Converter
- db db.DB
- accountModule *account.Module
- newUserFormHappyPath url.Values
-}
-
-/*
- TEST INFRASTRUCTURE
-*/
-
-// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
-func (suite *AccountUpdateTestSuite) SetupSuite() {
- // some of our subsequent entities need a log so create this here
- log := logrus.New()
- log.SetLevel(logrus.TraceLevel)
- suite.log = log
-
- suite.testAccountLocal = &gtsmodel.Account{
- ID: uuid.NewString(),
- Username: "test_user",
- }
-
- // can use this test application throughout
- suite.testApplication = &gtsmodel.Application{
- ID: "weeweeeeeeeeeeeeee",
- Name: "a test application",
- Website: "https://some-application-website.com",
- RedirectURI: "http://localhost:8080",
- ClientID: "a-known-client-id",
- ClientSecret: "some-secret",
- Scopes: "read",
- VapidKey: "aaaaaa-aaaaaaaa-aaaaaaaaaaa",
- }
-
- // can use this test token throughout
- suite.testToken = &oauthmodels.Token{
- ClientID: "a-known-client-id",
- RedirectURI: "http://localhost:8080",
- Scope: "read",
- Code: "123456789",
- CodeCreateAt: time.Now(),
- CodeExpiresIn: time.Duration(10 * time.Minute),
- }
-
- // Direct config to local postgres instance
- c := config.Empty()
- c.Protocol = "http"
- c.Host = "localhost"
- c.DBConfig = &config.DBConfig{
- Type: "postgres",
- Address: "localhost",
- Port: 5432,
- User: "postgres",
- Password: "postgres",
- Database: "postgres",
- ApplicationName: "gotosocial",
- }
- c.MediaConfig = &config.MediaConfig{
- MaxImageSize: 2 << 20,
- }
- c.StorageConfig = &config.StorageConfig{
- Backend: "local",
- BasePath: "/tmp",
- ServeProtocol: "http",
- ServeHost: "localhost",
- ServeBasePath: "/fileserver/media",
- }
- suite.config = c
-
- // use an actual database for this, because it's just easier than mocking one out
- database, err := db.New(context.Background(), c, log)
- if err != nil {
- suite.FailNow(err.Error())
- }
- suite.db = database
-
- // we need to mock the oauth server because account creation needs it to create a new token
- suite.mockOauthServer = &oauth.MockServer{}
- suite.mockOauthServer.On("GenerateUserAccessToken", suite.testToken, suite.testApplication.ClientSecret, mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
- l := suite.log.WithField("func", "GenerateUserAccessToken")
- token := args.Get(0).(oauth2.TokenInfo)
- l.Infof("received token %+v", token)
- clientSecret := args.Get(1).(string)
- l.Infof("received clientSecret %+v", clientSecret)
- userID := args.Get(2).(string)
- l.Infof("received userID %+v", userID)
- }).Return(&models.Token{
- Code: "we're authorized now!",
- }, nil)
-
- suite.mockStorage = &storage.MockStorage{}
- // We don't need storage to do anything for these tests, so just simulate a success and do nothing -- we won't need to return anything from storage
- suite.mockStorage.On("StoreFileAt", mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(nil)
-
- // set a media handler because some handlers (eg update credentials) need to upload media (new header/avatar)
- suite.mediaHandler = media.New(suite.config, suite.db, suite.mockStorage, log)
-
- suite.mastoConverter = mastotypes.New(suite.config, suite.db)
-
- // and finally here's the thing we're actually testing!
- suite.accountModule = account.New(suite.config, suite.db, suite.mockOauthServer, suite.mediaHandler, suite.mastoConverter, suite.log).(*account.Module)
-}
-
-func (suite *AccountUpdateTestSuite) TearDownSuite() {
- if err := suite.db.Stop(context.Background()); err != nil {
- logrus.Panicf("error closing db connection: %s", err)
- }
-}
-
-// SetupTest creates a db connection and creates necessary tables before each test
-func (suite *AccountUpdateTestSuite) SetupTest() {
- // create all the tables we might need in thie suite
- models := []interface{}{
- &gtsmodel.User{},
- &gtsmodel.Account{},
- &gtsmodel.Follow{},
- &gtsmodel.FollowRequest{},
- &gtsmodel.Status{},
- &gtsmodel.Application{},
- &gtsmodel.EmailDomainBlock{},
- &gtsmodel.MediaAttachment{},
- }
- for _, m := range models {
- if err := suite.db.CreateTable(m); err != nil {
- logrus.Panicf("db connection error: %s", err)
- }
- }
-
- // form to submit for happy path account create requests -- this will be changed inside tests so it's better to set it before each test
- suite.newUserFormHappyPath = url.Values{
- "reason": []string{"a very good reason that's at least 40 characters i swear"},
- "username": []string{"test_user"},
- "email": []string{"user@example.org"},
- "password": []string{"very-strong-password"},
- "agreement": []string{"true"},
- "locale": []string{"en"},
- }
-
- // same with accounts config
- suite.config.AccountsConfig = &config.AccountsConfig{
- OpenRegistration: true,
- RequireApproval: true,
- ReasonRequired: true,
- }
-}
-
-// TearDownTest drops tables to make sure there's no data in the db
-func (suite *AccountUpdateTestSuite) TearDownTest() {
-
- // remove all the tables we might have used so it's clear for the next test
- models := []interface{}{
- &gtsmodel.User{},
- &gtsmodel.Account{},
- &gtsmodel.Follow{},
- &gtsmodel.FollowRequest{},
- &gtsmodel.Status{},
- &gtsmodel.Application{},
- &gtsmodel.EmailDomainBlock{},
- &gtsmodel.MediaAttachment{},
- }
- for _, m := range models {
- if err := suite.db.DropTable(m); err != nil {
- logrus.Panicf("error dropping table: %s", err)
- }
- }
-}
-
-/*
- ACTUAL TESTS
-*/
-
-/*
- TESTING: AccountUpdateCredentialsPATCHHandler
-*/
-
-func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler() {
-
- // put test local account in db
- err := suite.db.Put(suite.testAccountLocal)
- assert.NoError(suite.T(), err)
-
- // attach avatar to request form
- avatarFile, err := os.Open("../../media/test/test-jpeg.jpg")
- assert.NoError(suite.T(), err)
- body := &bytes.Buffer{}
- writer := multipart.NewWriter(body)
-
- avatarPart, err := writer.CreateFormFile("avatar", "test-jpeg.jpg")
- assert.NoError(suite.T(), err)
-
- _, err = io.Copy(avatarPart, avatarFile)
- assert.NoError(suite.T(), err)
-
- err = avatarFile.Close()
- assert.NoError(suite.T(), err)
-
- // set display name to a new value
- displayNamePart, err := writer.CreateFormField("display_name")
- assert.NoError(suite.T(), err)
-
- _, err = io.Copy(displayNamePart, bytes.NewBufferString("test_user_wohoah"))
- assert.NoError(suite.T(), err)
-
- // set locked to true
- lockedPart, err := writer.CreateFormField("locked")
- assert.NoError(suite.T(), err)
-
- _, err = io.Copy(lockedPart, bytes.NewBufferString("true"))
- assert.NoError(suite.T(), err)
-
- // close the request writer, the form is now prepared
- err = writer.Close()
- assert.NoError(suite.T(), err)
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccountLocal)
- ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
- ctx.Request = httptest.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:8080/%s", account.UpdateCredentialsPath), body) // the endpoint we're hitting
- ctx.Request.Header.Set("Content-Type", writer.FormDataContentType())
- suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx)
-
- // check response
-
- // 1. we should have OK because our request was valid
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- // 2. we should have an error message in the result body
- result := recorder.Result()
- defer result.Body.Close()
- // TODO: implement proper checks here
- //
- // b, err := ioutil.ReadAll(result.Body)
- // assert.NoError(suite.T(), err)
- // assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b))
-}
-
-func TestAccountUpdateTestSuite(t *testing.T) {
- suite.Run(t, new(AccountUpdateTestSuite))
-}
diff --git a/internal/apimodule/account/test/accountverify_test.go b/internal/apimodule/account/test/accountverify_test.go
deleted file mode 100644
index 223a0c145..000000000
--- a/internal/apimodule/account/test/accountverify_test.go
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package account
diff --git a/internal/apimodule/admin/admin.go b/internal/apimodule/admin/admin.go
deleted file mode 100644
index 2ebe9c7a7..000000000
--- a/internal/apimodule/admin/admin.go
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package admin
-
-import (
- "fmt"
- "net/http"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/router"
-)
-
-const (
- // BasePath is the base API path for this module
- BasePath = "/api/v1/admin"
- // EmojiPath is used for posting/deleting custom emojis
- EmojiPath = BasePath + "/custom_emojis"
-)
-
-// Module implements the ClientAPIModule interface for admin-related actions (reports, emojis, etc)
-type Module struct {
- config *config.Config
- db db.DB
- mediaHandler media.Handler
- mastoConverter mastotypes.Converter
- log *logrus.Logger
-}
-
-// New returns a new admin module
-func New(config *config.Config, db db.DB, mediaHandler media.Handler, mastoConverter mastotypes.Converter, log *logrus.Logger) apimodule.ClientAPIModule {
- return &Module{
- config: config,
- db: db,
- mediaHandler: mediaHandler,
- mastoConverter: mastoConverter,
- log: log,
- }
-}
-
-// Route attaches all routes from this module to the given router
-func (m *Module) Route(r router.Router) error {
- r.AttachHandler(http.MethodPost, EmojiPath, m.emojiCreatePOSTHandler)
- return nil
-}
-
-// CreateTables creates the necessary tables for this module in the given database
-func (m *Module) CreateTables(db db.DB) error {
- models := []interface{}{
- &gtsmodel.User{},
- &gtsmodel.Account{},
- &gtsmodel.Follow{},
- &gtsmodel.FollowRequest{},
- &gtsmodel.Status{},
- &gtsmodel.Application{},
- &gtsmodel.EmailDomainBlock{},
- &gtsmodel.MediaAttachment{},
- &gtsmodel.Emoji{},
- }
-
- for _, m := range models {
- if err := db.CreateTable(m); err != nil {
- return fmt.Errorf("error creating table: %s", err)
- }
- }
- return nil
-}
diff --git a/internal/apimodule/admin/emojicreate.go b/internal/apimodule/admin/emojicreate.go
deleted file mode 100644
index 49e5492dd..000000000
--- a/internal/apimodule/admin/emojicreate.go
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package admin
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-func (m *Module) emojiCreatePOSTHandler(c *gin.Context) {
- l := m.log.WithFields(logrus.Fields{
- "func": "emojiCreatePOSTHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
-
- // make sure we're authed with an admin account
- authed, err := oauth.MustAuth(c, true, true, true, true) // posting a status is serious business so we want *everything*
- if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
- return
- }
- if !authed.User.Admin {
- l.Debugf("user %s not an admin", authed.User.ID)
- c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
- return
- }
-
- // extract the media create form from the request context
- l.Tracef("parsing request form: %+v", c.Request.Form)
- form := &mastotypes.EmojiCreateRequest{}
- if err := c.ShouldBind(form); err != nil {
- l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
- return
- }
-
- // Give the fields on the request form a first pass to make sure the request is superficially valid.
- l.Tracef("validating form %+v", form)
- if err := validateCreateEmoji(form); err != nil {
- l.Debugf("error validating form: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- // open the emoji and extract the bytes from it
- f, err := form.Image.Open()
- if err != nil {
- l.Debugf("error opening emoji: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not open provided emoji: %s", err)})
- return
- }
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, f)
- if err != nil {
- l.Debugf("error reading emoji: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not read provided emoji: %s", err)})
- return
- }
- if size == 0 {
- l.Debug("could not read provided emoji: size 0 bytes")
- c.JSON(http.StatusBadRequest, gin.H{"error": "could not read provided emoji: size 0 bytes"})
- return
- }
-
- // allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using
- emoji, err := m.mediaHandler.ProcessLocalEmoji(buf.Bytes(), form.Shortcode)
- if err != nil {
- l.Debugf("error reading emoji: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not process emoji: %s", err)})
- return
- }
-
- mastoEmoji, err := m.mastoConverter.EmojiToMasto(emoji)
- if err != nil {
- l.Debugf("error converting emoji to mastotype: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not convert emoji: %s", err)})
- return
- }
-
- if err := m.db.Put(emoji); err != nil {
- l.Debugf("database error while processing emoji: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("database error while processing emoji: %s", err)})
- return
- }
-
- c.JSON(http.StatusOK, mastoEmoji)
-}
-
-func validateCreateEmoji(form *mastotypes.EmojiCreateRequest) error {
- // check there actually is an image attached and it's not size 0
- if form.Image == nil || form.Image.Size == 0 {
- return errors.New("no emoji given")
- }
-
- // a very superficial check to see if the media size limit is exceeded
- if form.Image.Size > media.EmojiMaxBytes {
- return fmt.Errorf("file size limit exceeded: limit is %d bytes but emoji was %d bytes", media.EmojiMaxBytes, form.Image.Size)
- }
-
- return util.ValidateEmojiShortcode(form.Shortcode)
-}
diff --git a/internal/apimodule/apimodule.go b/internal/apimodule/apimodule.go
deleted file mode 100644
index 6d7dbdb83..000000000
--- a/internal/apimodule/apimodule.go
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// Package apimodule is basically a wrapper for a lot of modules (in subdirectories) that satisfy the ClientAPIModule interface.
-package apimodule
-
-import (
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/router"
-)
-
-// ClientAPIModule represents a chunk of code (usually contained in a single package) that adds a set
-// of functionalities and/or side effects to a router, by mapping routes and/or middlewares onto it--in other words, a REST API ;)
-// A ClientAPIMpdule with routes corresponds roughly to one main path of the gotosocial REST api, for example /api/v1/accounts/ or /oauth/
-type ClientAPIModule interface {
- Route(s router.Router) error
- CreateTables(db db.DB) error
-}
diff --git a/internal/apimodule/app/app.go b/internal/apimodule/app/app.go
deleted file mode 100644
index 518192758..000000000
--- a/internal/apimodule/app/app.go
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package app
-
-import (
- "fmt"
- "net/http"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/router"
-)
-
-// BasePath is the base path for this api module
-const BasePath = "/api/v1/apps"
-
-// Module implements the ClientAPIModule interface for requests relating to registering/removing applications
-type Module struct {
- server oauth.Server
- db db.DB
- mastoConverter mastotypes.Converter
- log *logrus.Logger
-}
-
-// New returns a new auth module
-func New(srv oauth.Server, db db.DB, mastoConverter mastotypes.Converter, log *logrus.Logger) apimodule.ClientAPIModule {
- return &Module{
- server: srv,
- db: db,
- mastoConverter: mastoConverter,
- log: log,
- }
-}
-
-// Route satisfies the RESTAPIModule interface
-func (m *Module) Route(s router.Router) error {
- s.AttachHandler(http.MethodPost, BasePath, m.AppsPOSTHandler)
- return nil
-}
-
-// CreateTables creates the necessary tables for this module in the given database
-func (m *Module) CreateTables(db db.DB) error {
- models := []interface{}{
- &oauth.Client{},
- &oauth.Token{},
- &gtsmodel.User{},
- &gtsmodel.Account{},
- &gtsmodel.Application{},
- }
-
- for _, m := range models {
- if err := db.CreateTable(m); err != nil {
- return fmt.Errorf("error creating table: %s", err)
- }
- }
- return nil
-}
diff --git a/internal/apimodule/app/appcreate.go b/internal/apimodule/app/appcreate.go
deleted file mode 100644
index 99b79d470..000000000
--- a/internal/apimodule/app/appcreate.go
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package app
-
-import (
- "fmt"
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/google/uuid"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-// AppsPOSTHandler should be served at https://example.org/api/v1/apps
-// It is equivalent to: https://docs.joinmastodon.org/methods/apps/
-func (m *Module) AppsPOSTHandler(c *gin.Context) {
- l := m.log.WithField("func", "AppsPOSTHandler")
- l.Trace("entering AppsPOSTHandler")
-
- form := &mastotypes.ApplicationPOSTRequest{}
- if err := c.ShouldBind(form); err != nil {
- c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
- return
- }
-
- // permitted length for most fields
- permittedLength := 64
- // redirect can be a bit bigger because we probably need to encode data in the redirect uri
- permittedRedirect := 256
-
- // check lengths of fields before proceeding so the user can't spam huge entries into the database
- if len(form.ClientName) > permittedLength {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", permittedLength)})
- return
- }
- if len(form.Website) > permittedLength {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("website must be less than %d bytes", permittedLength)})
- return
- }
- if len(form.RedirectURIs) > permittedRedirect {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("redirect_uris must be less than %d bytes", permittedRedirect)})
- return
- }
- if len(form.Scopes) > permittedLength {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("scopes must be less than %d bytes", permittedLength)})
- return
- }
-
- // set default 'read' for scopes if it's not set, this follows the default of the mastodon api https://docs.joinmastodon.org/methods/apps/
- var scopes string
- if form.Scopes == "" {
- scopes = "read"
- } else {
- scopes = form.Scopes
- }
-
- // generate new IDs for this application and its associated client
- clientID := uuid.NewString()
- clientSecret := uuid.NewString()
- vapidKey := uuid.NewString()
-
- // generate the application to put in the database
- app := &gtsmodel.Application{
- Name: form.ClientName,
- Website: form.Website,
- RedirectURI: form.RedirectURIs,
- ClientID: clientID,
- ClientSecret: clientSecret,
- Scopes: scopes,
- VapidKey: vapidKey,
- }
-
- // chuck it in the db
- if err := m.db.Put(app); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- // now we need to model an oauth client from the application that the oauth library can use
- oc := &oauth.Client{
- ID: clientID,
- Secret: clientSecret,
- Domain: form.RedirectURIs,
- UserID: "", // This client isn't yet associated with a specific user, it's just an app client right now
- }
-
- // chuck it in the db
- if err := m.db.Put(oc); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- mastoApp, err := m.mastoConverter.AppToMastoSensitive(app)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- // done, return the new app information per the spec here: https://docs.joinmastodon.org/methods/apps/
- c.JSON(http.StatusOK, mastoApp)
-}
diff --git a/internal/apimodule/app/test/app_test.go b/internal/apimodule/app/test/app_test.go
deleted file mode 100644
index d45b04e74..000000000
--- a/internal/apimodule/app/test/app_test.go
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package app
-
-// TODO: write tests
diff --git a/internal/apimodule/auth/auth.go b/internal/apimodule/auth/auth.go
deleted file mode 100644
index 341805b40..000000000
--- a/internal/apimodule/auth/auth.go
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package auth
-
-import (
- "fmt"
- "net/http"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/router"
-)
-
-const (
- // AuthSignInPath is the API path for users to sign in through
- AuthSignInPath = "/auth/sign_in"
- // OauthTokenPath is the API path to use for granting token requests to users with valid credentials
- OauthTokenPath = "/oauth/token"
- // OauthAuthorizePath is the API path for authorization requests (eg., authorize this app to act on my behalf as a user)
- OauthAuthorizePath = "/oauth/authorize"
-)
-
-// Module implements the ClientAPIModule interface for
-type Module struct {
- server oauth.Server
- db db.DB
- log *logrus.Logger
-}
-
-// New returns a new auth module
-func New(srv oauth.Server, db db.DB, log *logrus.Logger) apimodule.ClientAPIModule {
- return &Module{
- server: srv,
- db: db,
- log: log,
- }
-}
-
-// Route satisfies the RESTAPIModule interface
-func (m *Module) Route(s router.Router) error {
- s.AttachHandler(http.MethodGet, AuthSignInPath, m.SignInGETHandler)
- s.AttachHandler(http.MethodPost, AuthSignInPath, m.SignInPOSTHandler)
-
- s.AttachHandler(http.MethodPost, OauthTokenPath, m.TokenPOSTHandler)
-
- s.AttachHandler(http.MethodGet, OauthAuthorizePath, m.AuthorizeGETHandler)
- s.AttachHandler(http.MethodPost, OauthAuthorizePath, m.AuthorizePOSTHandler)
-
- s.AttachMiddleware(m.OauthTokenMiddleware)
- return nil
-}
-
-// CreateTables creates the necessary tables for this module in the given database
-func (m *Module) CreateTables(db db.DB) error {
- models := []interface{}{
- &oauth.Client{},
- &oauth.Token{},
- &gtsmodel.User{},
- &gtsmodel.Account{},
- &gtsmodel.Application{},
- }
-
- for _, m := range models {
- if err := db.CreateTable(m); err != nil {
- return fmt.Errorf("error creating table: %s", err)
- }
- }
- return nil
-}
diff --git a/internal/apimodule/auth/authorize.go b/internal/apimodule/auth/authorize.go
deleted file mode 100644
index 4bc1991ac..000000000
--- a/internal/apimodule/auth/authorize.go
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package auth
-
-import (
- "errors"
- "fmt"
- "net/http"
- "net/url"
-
- "github.com/gin-contrib/sessions"
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
-)
-
-// AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize
-// The idea here is to present an oauth authorize page to the user, with a button
-// that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user
-func (m *Module) AuthorizeGETHandler(c *gin.Context) {
- l := m.log.WithField("func", "AuthorizeGETHandler")
- s := sessions.Default(c)
-
- // UserID will be set in the session by AuthorizePOSTHandler if the caller has already gone through the authentication flow
- // If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page.
- userID, ok := s.Get("userid").(string)
- if !ok || userID == "" {
- l.Trace("userid was empty, parsing form then redirecting to sign in page")
- if err := parseAuthForm(c, l); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- } else {
- c.Redirect(http.StatusFound, AuthSignInPath)
- }
- return
- }
-
- // We can use the client_id on the session to retrieve info about the app associated with the client_id
- clientID, ok := s.Get("client_id").(string)
- if !ok || clientID == "" {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "no client_id found in session"})
- return
- }
- app := &gtsmodel.Application{
- ClientID: clientID,
- }
- if err := m.db.GetWhere("client_id", app.ClientID, app); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)})
- return
- }
-
- // we can also use the userid of the user to fetch their username from the db to greet them nicely <3
- user := &gtsmodel.User{
- ID: userID,
- }
- if err := m.db.GetByID(user.ID, user); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- acct := &gtsmodel.Account{
- ID: user.AccountID,
- }
-
- if err := m.db.GetByID(acct.ID, acct); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- // Finally we should also get the redirect and scope of this particular request, as stored in the session.
- redirect, ok := s.Get("redirect_uri").(string)
- if !ok || redirect == "" {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "no redirect_uri found in session"})
- return
- }
- scope, ok := s.Get("scope").(string)
- if !ok || scope == "" {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "no scope found in session"})
- return
- }
-
- // the authorize template will display a form to the user where they can get some information
- // about the app that's trying to authorize, and the scope of the request.
- // They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler
- l.Trace("serving authorize html")
- c.HTML(http.StatusOK, "authorize.tmpl", gin.H{
- "appname": app.Name,
- "appwebsite": app.Website,
- "redirect": redirect,
- "scope": scope,
- "user": acct.Username,
- })
-}
-
-// AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize
-// At this point we assume that the user has A) logged in and B) accepted that the app should act for them,
-// so we should proceed with the authentication flow and generate an oauth token for them if we can.
-// See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user
-func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
- l := m.log.WithField("func", "AuthorizePOSTHandler")
- s := sessions.Default(c)
-
- // At this point we know the user has said 'yes' to allowing the application and oauth client
- // work for them, so we can set the
-
- // We need to retrieve the original form submitted to the authorizeGEThandler, and
- // recreate it on the request so that it can be used further by the oauth2 library.
- // So first fetch all the values from the session.
- forceLogin, ok := s.Get("force_login").(string)
- if !ok {
- c.JSON(http.StatusBadRequest, gin.H{"error": "session missing force_login"})
- return
- }
- responseType, ok := s.Get("response_type").(string)
- if !ok || responseType == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "session missing response_type"})
- return
- }
- clientID, ok := s.Get("client_id").(string)
- if !ok || clientID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "session missing client_id"})
- return
- }
- redirectURI, ok := s.Get("redirect_uri").(string)
- if !ok || redirectURI == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "session missing redirect_uri"})
- return
- }
- scope, ok := s.Get("scope").(string)
- if !ok {
- c.JSON(http.StatusBadRequest, gin.H{"error": "session missing scope"})
- return
- }
- userID, ok := s.Get("userid").(string)
- if !ok {
- c.JSON(http.StatusBadRequest, gin.H{"error": "session missing userid"})
- return
- }
- // we're done with the session so we can clear it now
- s.Clear()
-
- // now set the values on the request
- values := url.Values{}
- values.Set("force_login", forceLogin)
- values.Set("response_type", responseType)
- values.Set("client_id", clientID)
- values.Set("redirect_uri", redirectURI)
- values.Set("scope", scope)
- values.Set("userid", userID)
- c.Request.Form = values
- l.Tracef("values on request set to %+v", c.Request.Form)
-
- // and proceed with authorization using the oauth2 library
- if err := m.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- }
-}
-
-// parseAuthForm parses the OAuthAuthorize form in the gin context, and stores
-// the values in the form into the session.
-func parseAuthForm(c *gin.Context, l *logrus.Entry) error {
- s := sessions.Default(c)
-
- // first make sure they've filled out the authorize form with the required values
- form := &mastotypes.OAuthAuthorize{}
- if err := c.ShouldBind(form); err != nil {
- return err
- }
- l.Tracef("parsed form: %+v", form)
-
- // these fields are *required* so check 'em
- if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" {
- return errors.New("missing one of: response_type, client_id or redirect_uri")
- }
-
- // set default scope to read
- if form.Scope == "" {
- form.Scope = "read"
- }
-
- // save these values from the form so we can use them elsewhere in the session
- s.Set("force_login", form.ForceLogin)
- s.Set("response_type", form.ResponseType)
- s.Set("client_id", form.ClientID)
- s.Set("redirect_uri", form.RedirectURI)
- s.Set("scope", form.Scope)
- return s.Save()
-}
diff --git a/internal/apimodule/auth/middleware.go b/internal/apimodule/auth/middleware.go
deleted file mode 100644
index 1d9a85993..000000000
--- a/internal/apimodule/auth/middleware.go
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package auth
-
-import (
- "github.com/gin-gonic/gin"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-// OauthTokenMiddleware checks if the client has presented a valid oauth Bearer token.
-// If so, it will check the User that the token belongs to, and set that in the context of
-// the request. Then, it will look up the account for that user, and set that in the request too.
-// If user or account can't be found, then the handler won't *fail*, in case the server wants to allow
-// public requests that don't have a Bearer token set (eg., for public instance information and so on).
-func (m *Module) OauthTokenMiddleware(c *gin.Context) {
- l := m.log.WithField("func", "ValidatePassword")
- l.Trace("entering OauthTokenMiddleware")
-
- ti, err := m.server.ValidationBearerToken(c.Request)
- if err != nil {
- l.Trace("no valid token presented: continuing with unauthenticated request")
- return
- }
- c.Set(oauth.SessionAuthorizedToken, ti)
- l.Tracef("set gin context %s to %+v", oauth.SessionAuthorizedToken, ti)
-
- // check for user-level token
- if uid := ti.GetUserID(); uid != "" {
- l.Tracef("authenticated user %s with bearer token, scope is %s", uid, ti.GetScope())
-
- // fetch user's and account for this user id
- user := &gtsmodel.User{}
- if err := m.db.GetByID(uid, user); err != nil || user == nil {
- l.Warnf("no user found for validated uid %s", uid)
- return
- }
- c.Set(oauth.SessionAuthorizedUser, user)
- l.Tracef("set gin context %s to %+v", oauth.SessionAuthorizedUser, user)
-
- acct := &gtsmodel.Account{}
- if err := m.db.GetByID(user.AccountID, acct); err != nil || acct == nil {
- l.Warnf("no account found for validated user %s", uid)
- return
- }
- c.Set(oauth.SessionAuthorizedAccount, acct)
- l.Tracef("set gin context %s to %+v", oauth.SessionAuthorizedAccount, acct)
- }
-
- // check for application token
- if cid := ti.GetClientID(); cid != "" {
- l.Tracef("authenticated client %s with bearer token, scope is %s", cid, ti.GetScope())
- app := &gtsmodel.Application{}
- if err := m.db.GetWhere("client_id", cid, app); err != nil {
- l.Tracef("no app found for client %s", cid)
- }
- c.Set(oauth.SessionAuthorizedApplication, app)
- l.Tracef("set gin context %s to %+v", oauth.SessionAuthorizedApplication, app)
- }
-}
diff --git a/internal/apimodule/auth/signin.go b/internal/apimodule/auth/signin.go
deleted file mode 100644
index 44de0891c..000000000
--- a/internal/apimodule/auth/signin.go
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package auth
-
-import (
- "errors"
- "net/http"
-
- "github.com/gin-contrib/sessions"
- "github.com/gin-gonic/gin"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "golang.org/x/crypto/bcrypt"
-)
-
-// login just wraps a form-submitted username (we want an email) and password
-type login struct {
- Email string `form:"username"`
- Password string `form:"password"`
-}
-
-// SignInGETHandler should be served at https://example.org/auth/sign_in.
-// The idea is to present a sign in page to the user, where they can enter their username and password.
-// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler
-func (m *Module) SignInGETHandler(c *gin.Context) {
- m.log.WithField("func", "SignInGETHandler").Trace("serving sign in html")
- c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
-}
-
-// SignInPOSTHandler should be served at https://example.org/auth/sign_in.
-// The idea is to present a sign in page to the user, where they can enter their username and password.
-// The handler will then redirect to the auth handler served at /auth
-func (m *Module) SignInPOSTHandler(c *gin.Context) {
- l := m.log.WithField("func", "SignInPOSTHandler")
- s := sessions.Default(c)
- form := &login{}
- if err := c.ShouldBind(form); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- l.Tracef("parsed form: %+v", form)
-
- userid, err := m.ValidatePassword(form.Email, form.Password)
- if err != nil {
- c.String(http.StatusForbidden, err.Error())
- return
- }
-
- s.Set("userid", userid)
- if err := s.Save(); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- l.Trace("redirecting to auth page")
- c.Redirect(http.StatusFound, OauthAuthorizePath)
-}
-
-// ValidatePassword takes an email address and a password.
-// The goal is to authenticate the password against the one for that email
-// address stored in the database. If OK, we return the userid (a uuid) for that user,
-// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db.
-func (m *Module) ValidatePassword(email string, password string) (userid string, err error) {
- l := m.log.WithField("func", "ValidatePassword")
-
- // make sure an email/password was provided and bail if not
- if email == "" || password == "" {
- l.Debug("email or password was not provided")
- return incorrectPassword()
- }
-
- // first we select the user from the database based on email address, bail if no user found for that email
- gtsUser := &gtsmodel.User{}
-
- if err := m.db.GetWhere("email", email, gtsUser); err != nil {
- l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
- return incorrectPassword()
- }
-
- // make sure a password is actually set and bail if not
- if gtsUser.EncryptedPassword == "" {
- l.Warnf("encrypted password for user %s was empty for some reason", gtsUser.Email)
- return incorrectPassword()
- }
-
- // compare the provided password with the encrypted one from the db, bail if they don't match
- if err := bcrypt.CompareHashAndPassword([]byte(gtsUser.EncryptedPassword), []byte(password)); err != nil {
- l.Debugf("password hash didn't match for user %s during login attempt: %s", gtsUser.Email, err)
- return incorrectPassword()
- }
-
- // If we've made it this far the email/password is correct, so we can just return the id of the user.
- userid = gtsUser.ID
- l.Tracef("returning (%s, %s)", userid, err)
- return
-}
-
-// incorrectPassword is just a little helper function to use in the ValidatePassword function
-func incorrectPassword() (string, error) {
- return "", errors.New("password/email combination was incorrect")
-}
diff --git a/internal/apimodule/auth/test/auth_test.go b/internal/apimodule/auth/test/auth_test.go
deleted file mode 100644
index 2c272e985..000000000
--- a/internal/apimodule/auth/test/auth_test.go
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package auth
-
-import (
- "context"
- "fmt"
- "testing"
-
- "github.com/google/uuid"
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "golang.org/x/crypto/bcrypt"
-)
-
-type AuthTestSuite struct {
- suite.Suite
- oauthServer oauth.Server
- db db.DB
- testAccount *gtsmodel.Account
- testApplication *gtsmodel.Application
- testUser *gtsmodel.User
- testClient *oauth.Client
- config *config.Config
-}
-
-// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
-func (suite *AuthTestSuite) SetupSuite() {
- c := config.Empty()
- // we're running on localhost without https so set the protocol to http
- c.Protocol = "http"
- // just for testing
- c.Host = "localhost:8080"
- // because go tests are run within the test package directory, we need to fiddle with the templateconfig
- // basedir in a way that we wouldn't normally have to do when running the binary, in order to make
- // the templates actually load
- c.TemplateConfig.BaseDir = "../../../web/template/"
- c.DBConfig = &config.DBConfig{
- Type: "postgres",
- Address: "localhost",
- Port: 5432,
- User: "postgres",
- Password: "postgres",
- Database: "postgres",
- ApplicationName: "gotosocial",
- }
- suite.config = c
-
- encryptedPassword, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
- if err != nil {
- logrus.Panicf("error encrypting user pass: %s", err)
- }
-
- acctID := uuid.NewString()
-
- suite.testAccount = &gtsmodel.Account{
- ID: acctID,
- Username: "test_user",
- }
- suite.testUser = &gtsmodel.User{
- EncryptedPassword: string(encryptedPassword),
- Email: "user@example.org",
- AccountID: acctID,
- }
- suite.testClient = &oauth.Client{
- ID: "a-known-client-id",
- Secret: "some-secret",
- Domain: fmt.Sprintf("%s://%s", c.Protocol, c.Host),
- }
- suite.testApplication = &gtsmodel.Application{
- Name: "a test application",
- Website: "https://some-application-website.com",
- RedirectURI: "http://localhost:8080",
- ClientID: "a-known-client-id",
- ClientSecret: "some-secret",
- Scopes: "read",
- VapidKey: uuid.NewString(),
- }
-}
-
-// SetupTest creates a postgres connection and creates the oauth_clients table before each test
-func (suite *AuthTestSuite) SetupTest() {
-
- log := logrus.New()
- log.SetLevel(logrus.TraceLevel)
- db, err := db.New(context.Background(), suite.config, log)
- if err != nil {
- logrus.Panicf("error creating database connection: %s", err)
- }
-
- suite.db = db
-
- models := []interface{}{
- &oauth.Client{},
- &oauth.Token{},
- &gtsmodel.User{},
- &gtsmodel.Account{},
- &gtsmodel.Application{},
- }
-
- for _, m := range models {
- if err := suite.db.CreateTable(m); err != nil {
- logrus.Panicf("db connection error: %s", err)
- }
- }
-
- suite.oauthServer = oauth.New(suite.db, log)
-
- if err := suite.db.Put(suite.testAccount); err != nil {
- logrus.Panicf("could not insert test account into db: %s", err)
- }
- if err := suite.db.Put(suite.testUser); err != nil {
- logrus.Panicf("could not insert test user into db: %s", err)
- }
- if err := suite.db.Put(suite.testClient); err != nil {
- logrus.Panicf("could not insert test client into db: %s", err)
- }
- if err := suite.db.Put(suite.testApplication); err != nil {
- logrus.Panicf("could not insert test application into db: %s", err)
- }
-
-}
-
-// TearDownTest drops the oauth_clients table and closes the pg connection after each test
-func (suite *AuthTestSuite) TearDownTest() {
- models := []interface{}{
- &oauth.Client{},
- &oauth.Token{},
- &gtsmodel.User{},
- &gtsmodel.Account{},
- &gtsmodel.Application{},
- }
- for _, m := range models {
- if err := suite.db.DropTable(m); err != nil {
- logrus.Panicf("error dropping table: %s", err)
- }
- }
- if err := suite.db.Stop(context.Background()); err != nil {
- logrus.Panicf("error closing db connection: %s", err)
- }
- suite.db = nil
-}
-
-func TestAuthTestSuite(t *testing.T) {
- suite.Run(t, new(AuthTestSuite))
-}
diff --git a/internal/apimodule/auth/token.go b/internal/apimodule/auth/token.go
deleted file mode 100644
index c531a3009..000000000
--- a/internal/apimodule/auth/token.go
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package auth
-
-import (
- "net/http"
-
- "github.com/gin-gonic/gin"
-)
-
-// TokenPOSTHandler should be served as a POST at https://example.org/oauth/token
-// The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs.
-// See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token
-func (m *Module) TokenPOSTHandler(c *gin.Context) {
- l := m.log.WithField("func", "TokenPOSTHandler")
- l.Trace("entered TokenPOSTHandler")
- if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- }
-}
diff --git a/internal/apimodule/fileserver/fileserver.go b/internal/apimodule/fileserver/fileserver.go
deleted file mode 100644
index 7651c8cc1..000000000
--- a/internal/apimodule/fileserver/fileserver.go
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package fileserver
-
-import (
- "fmt"
- "net/http"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/router"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
-)
-
-const (
- // AccountIDKey is the url key for account id (an account uuid)
- AccountIDKey = "account_id"
- // MediaTypeKey is the url key for media type (usually something like attachment or header etc)
- MediaTypeKey = "media_type"
- // MediaSizeKey is the url key for the desired media size--original/small/static
- MediaSizeKey = "media_size"
- // FileNameKey is the actual filename being sought. Will usually be a UUID then something like .jpeg
- FileNameKey = "file_name"
-)
-
-// FileServer implements the RESTAPIModule interface.
-// The goal here is to serve requested media files if the gotosocial server is configured to use local storage.
-type FileServer struct {
- config *config.Config
- db db.DB
- storage storage.Storage
- log *logrus.Logger
- storageBase string
-}
-
-// New returns a new fileServer module
-func New(config *config.Config, db db.DB, storage storage.Storage, log *logrus.Logger) apimodule.ClientAPIModule {
- return &FileServer{
- config: config,
- db: db,
- storage: storage,
- log: log,
- storageBase: config.StorageConfig.ServeBasePath,
- }
-}
-
-// Route satisfies the RESTAPIModule interface
-func (m *FileServer) Route(s router.Router) error {
- s.AttachHandler(http.MethodGet, fmt.Sprintf("%s/:%s/:%s/:%s/:%s", m.storageBase, AccountIDKey, MediaTypeKey, MediaSizeKey, FileNameKey), m.ServeFile)
- return nil
-}
-
-// CreateTables populates necessary tables in the given DB
-func (m *FileServer) CreateTables(db db.DB) error {
- models := []interface{}{
- &gtsmodel.MediaAttachment{},
- }
-
- for _, m := range models {
- if err := db.CreateTable(m); err != nil {
- return fmt.Errorf("error creating table: %s", err)
- }
- }
- return nil
-}
diff --git a/internal/apimodule/fileserver/servefile.go b/internal/apimodule/fileserver/servefile.go
deleted file mode 100644
index 0421c5095..000000000
--- a/internal/apimodule/fileserver/servefile.go
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package fileserver
-
-import (
- "bytes"
- "net/http"
- "strings"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
-)
-
-// ServeFile is for serving attachments, headers, and avatars to the requester from instance storage.
-//
-// Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found".
-// Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything.
-func (m *FileServer) ServeFile(c *gin.Context) {
- l := m.log.WithFields(logrus.Fields{
- "func": "ServeFile",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Trace("received request")
-
- // We use request params to check what to pull out of the database/storage so check everything. A request URL should be formatted as follows:
- // "https://example.org/fileserver/[ACCOUNT_ID]/[MEDIA_TYPE]/[MEDIA_SIZE]/[FILE_NAME]"
- // "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension.
- accountID := c.Param(AccountIDKey)
- if accountID == "" {
- l.Debug("missing accountID from request")
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- mediaType := c.Param(MediaTypeKey)
- if mediaType == "" {
- l.Debug("missing mediaType from request")
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- mediaSize := c.Param(MediaSizeKey)
- if mediaSize == "" {
- l.Debug("missing mediaSize from request")
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- fileName := c.Param(FileNameKey)
- if fileName == "" {
- l.Debug("missing fileName from request")
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- // Only serve media types that are defined in our internal media module
- switch mediaType {
- case media.MediaHeader, media.MediaAvatar, media.MediaAttachment:
- m.serveAttachment(c, accountID, mediaType, mediaSize, fileName)
- return
- case media.MediaEmoji:
- m.serveEmoji(c, accountID, mediaType, mediaSize, fileName)
- return
- }
- l.Debugf("mediatype %s not recognized", mediaType)
- c.String(http.StatusNotFound, "404 page not found")
-}
-
-func (m *FileServer) serveAttachment(c *gin.Context, accountID string, mediaType string, mediaSize string, fileName string) {
- l := m.log.WithFields(logrus.Fields{
- "func": "serveAttachment",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
-
- // This corresponds to original-sized image as it was uploaded, small (which is the thumbnail), or static
- switch mediaSize {
- case media.MediaOriginal, media.MediaSmall, media.MediaStatic:
- default:
- l.Debugf("mediasize %s not recognized", mediaSize)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- // derive the media id and the file extension from the last part of the request
- spl := strings.Split(fileName, ".")
- if len(spl) != 2 {
- l.Debugf("filename %s not parseable", fileName)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
- wantedMediaID := spl[0]
- fileExtension := spl[1]
- if wantedMediaID == "" || fileExtension == "" {
- l.Debugf("filename %s not parseable", fileName)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- // now we know the attachment ID that the caller is asking for we can use it to pull the attachment out of the db
- attachment := &gtsmodel.MediaAttachment{}
- if err := m.db.GetByID(wantedMediaID, attachment); err != nil {
- l.Debugf("attachment with id %s not retrievable: %s", wantedMediaID, err)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- // make sure the given account id owns the requested attachment
- if accountID != attachment.AccountID {
- l.Debugf("account %s does not own attachment with id %s", accountID, wantedMediaID)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- // now we can start preparing the response depending on whether we're serving a thumbnail or a larger attachment
- var storagePath string
- var contentType string
- var contentLength int
- switch mediaSize {
- case media.MediaOriginal:
- storagePath = attachment.File.Path
- contentType = attachment.File.ContentType
- contentLength = attachment.File.FileSize
- case media.MediaSmall:
- storagePath = attachment.Thumbnail.Path
- contentType = attachment.Thumbnail.ContentType
- contentLength = attachment.Thumbnail.FileSize
- }
-
- // use the path listed on the attachment we pulled out of the database to retrieve the object from storage
- attachmentBytes, err := m.storage.RetrieveFileFrom(storagePath)
- if err != nil {
- l.Debugf("error retrieving from storage: %s", err)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- l.Errorf("about to serve content length: %d attachment bytes is: %d", int64(contentLength), int64(len(attachmentBytes)))
-
- // finally we can return with all the information we derived above
- c.DataFromReader(http.StatusOK, int64(contentLength), contentType, bytes.NewReader(attachmentBytes), map[string]string{})
-}
-
-func (m *FileServer) serveEmoji(c *gin.Context, accountID string, mediaType string, mediaSize string, fileName string) {
- l := m.log.WithFields(logrus.Fields{
- "func": "serveEmoji",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
-
- // This corresponds to original-sized emoji as it was uploaded, or static
- switch mediaSize {
- case media.MediaOriginal, media.MediaStatic:
- default:
- l.Debugf("mediasize %s not recognized", mediaSize)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- // derive the media id and the file extension from the last part of the request
- spl := strings.Split(fileName, ".")
- if len(spl) != 2 {
- l.Debugf("filename %s not parseable", fileName)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
- wantedEmojiID := spl[0]
- fileExtension := spl[1]
- if wantedEmojiID == "" || fileExtension == "" {
- l.Debugf("filename %s not parseable", fileName)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- // now we know the attachment ID that the caller is asking for we can use it to pull the attachment out of the db
- emoji := &gtsmodel.Emoji{}
- if err := m.db.GetByID(wantedEmojiID, emoji); err != nil {
- l.Debugf("emoji with id %s not retrievable: %s", wantedEmojiID, err)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- // make sure the instance account id owns the requested emoji
- instanceAccount := &gtsmodel.Account{}
- if err := m.db.GetWhere("username", m.config.Host, instanceAccount); err != nil {
- l.Debugf("error fetching instance account: %s", err)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
- if accountID != instanceAccount.ID {
- l.Debugf("account %s does not own emoji with id %s", accountID, wantedEmojiID)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- // now we can start preparing the response depending on whether we're serving a thumbnail or a larger attachment
- var storagePath string
- var contentType string
- var contentLength int
- switch mediaSize {
- case media.MediaOriginal:
- storagePath = emoji.ImagePath
- contentType = emoji.ImageContentType
- contentLength = emoji.ImageFileSize
- case media.MediaStatic:
- storagePath = emoji.ImageStaticPath
- contentType = "image/png"
- contentLength = emoji.ImageStaticFileSize
- }
-
- // use the path listed on the emoji we pulled out of the database to retrieve the object from storage
- emojiBytes, err := m.storage.RetrieveFileFrom(storagePath)
- if err != nil {
- l.Debugf("error retrieving emoji from storage: %s", err)
- c.String(http.StatusNotFound, "404 page not found")
- return
- }
-
- // finally we can return with all the information we derived above
- c.DataFromReader(http.StatusOK, int64(contentLength), contentType, bytes.NewReader(emojiBytes), map[string]string{})
-}
diff --git a/internal/apimodule/fileserver/test/servefile_test.go b/internal/apimodule/fileserver/test/servefile_test.go
deleted file mode 100644
index 516e3528c..000000000
--- a/internal/apimodule/fileserver/test/servefile_test.go
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package test
-
-import (
- "context"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule/fileserver"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/testrig"
-)
-
-type ServeFileTestSuite struct {
- // standard suite interfaces
- suite.Suite
- config *config.Config
- db db.DB
- log *logrus.Logger
- storage storage.Storage
- mastoConverter mastotypes.Converter
- mediaHandler media.Handler
- oauthServer oauth.Server
-
- // standard suite models
- testTokens map[string]*oauth.Token
- testClients map[string]*oauth.Client
- testApplications map[string]*gtsmodel.Application
- testUsers map[string]*gtsmodel.User
- testAccounts map[string]*gtsmodel.Account
- testAttachments map[string]*gtsmodel.MediaAttachment
-
- // item being tested
- fileServer *fileserver.FileServer
-}
-
-/*
- TEST INFRASTRUCTURE
-*/
-
-func (suite *ServeFileTestSuite) SetupSuite() {
- // setup standard items
- suite.config = testrig.NewTestConfig()
- suite.db = testrig.NewTestDB()
- suite.log = testrig.NewTestLog()
- suite.storage = testrig.NewTestStorage()
- suite.mastoConverter = testrig.NewTestMastoConverter(suite.db)
- suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
-
- // setup module being tested
- suite.fileServer = fileserver.New(suite.config, suite.db, suite.storage, suite.log).(*fileserver.FileServer)
-}
-
-func (suite *ServeFileTestSuite) TearDownSuite() {
- if err := suite.db.Stop(context.Background()); err != nil {
- logrus.Panicf("error closing db connection: %s", err)
- }
-}
-
-func (suite *ServeFileTestSuite) SetupTest() {
- testrig.StandardDBSetup(suite.db)
- testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
- suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
- suite.testApplications = testrig.NewTestApplications()
- suite.testUsers = testrig.NewTestUsers()
- suite.testAccounts = testrig.NewTestAccounts()
- suite.testAttachments = testrig.NewTestAttachments()
-}
-
-func (suite *ServeFileTestSuite) TearDownTest() {
- testrig.StandardDBTeardown(suite.db)
- testrig.StandardStorageTeardown(suite.storage)
-}
-
-/*
- ACTUAL TESTS
-*/
-
-func (suite *ServeFileTestSuite) TestServeOriginalFileSuccessful() {
- targetAttachment, ok := suite.testAttachments["admin_account_status_1_attachment_1"]
- assert.True(suite.T(), ok)
- assert.NotNil(suite.T(), targetAttachment)
-
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Request = httptest.NewRequest(http.MethodGet, targetAttachment.URL, nil)
-
- // normally the router would populate these params from the path values,
- // but because we're calling the ServeFile function directly, we need to set them manually.
- ctx.Params = gin.Params{
- gin.Param{
- Key: fileserver.AccountIDKey,
- Value: targetAttachment.AccountID,
- },
- gin.Param{
- Key: fileserver.MediaTypeKey,
- Value: media.MediaAttachment,
- },
- gin.Param{
- Key: fileserver.MediaSizeKey,
- Value: media.MediaOriginal,
- },
- gin.Param{
- Key: fileserver.FileNameKey,
- Value: fmt.Sprintf("%s.jpeg", targetAttachment.ID),
- },
- }
-
- // call the function we're testing and check status code
- suite.fileServer.ServeFile(ctx)
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- b, err := ioutil.ReadAll(recorder.Body)
- assert.NoError(suite.T(), err)
- assert.NotNil(suite.T(), b)
-
- fileInStorage, err := suite.storage.RetrieveFileFrom(targetAttachment.File.Path)
- assert.NoError(suite.T(), err)
- assert.NotNil(suite.T(), fileInStorage)
- assert.Equal(suite.T(), b, fileInStorage)
-}
-
-func TestServeFileTestSuite(t *testing.T) {
- suite.Run(t, new(ServeFileTestSuite))
-}
diff --git a/internal/apimodule/media/media.go b/internal/apimodule/media/media.go
deleted file mode 100644
index 8fb9f16ec..000000000
--- a/internal/apimodule/media/media.go
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package media
-
-import (
- "fmt"
- "net/http"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/router"
-)
-
-// BasePath is the base API path for making media requests
-const BasePath = "/api/v1/media"
-
-// Module implements the ClientAPIModule interface for media
-type Module struct {
- mediaHandler media.Handler
- config *config.Config
- db db.DB
- mastoConverter mastotypes.Converter
- log *logrus.Logger
-}
-
-// New returns a new auth module
-func New(db db.DB, mediaHandler media.Handler, mastoConverter mastotypes.Converter, config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule {
- return &Module{
- mediaHandler: mediaHandler,
- config: config,
- db: db,
- mastoConverter: mastoConverter,
- log: log,
- }
-}
-
-// Route satisfies the RESTAPIModule interface
-func (m *Module) Route(s router.Router) error {
- s.AttachHandler(http.MethodPost, BasePath, m.MediaCreatePOSTHandler)
- return nil
-}
-
-// CreateTables populates necessary tables in the given DB
-func (m *Module) CreateTables(db db.DB) error {
- models := []interface{}{
- &gtsmodel.MediaAttachment{},
- }
-
- for _, m := range models {
- if err := db.CreateTable(m); err != nil {
- return fmt.Errorf("error creating table: %s", err)
- }
- }
- return nil
-}
diff --git a/internal/apimodule/media/mediacreate.go b/internal/apimodule/media/mediacreate.go
deleted file mode 100644
index ee713a471..000000000
--- a/internal/apimodule/media/mediacreate.go
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package media
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "net/http"
- "strconv"
- "strings"
-
- "github.com/gin-gonic/gin"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-// MediaCreatePOSTHandler handles requests to create/upload media attachments
-func (m *Module) MediaCreatePOSTHandler(c *gin.Context) {
- l := m.log.WithField("func", "statusCreatePOSTHandler")
- authed, err := oauth.MustAuth(c, true, true, true, true) // posting new media is serious business so we want *everything*
- if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
- return
- }
-
- // First check this user/account is permitted to create media
- // There's no point continuing otherwise.
- if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
- return
- }
-
- // extract the media create form from the request context
- l.Tracef("parsing request form: %s", c.Request.Form)
- form := &mastotypes.AttachmentRequest{}
- if err := c.ShouldBind(form); err != nil || form == nil {
- l.Debugf("could not parse form from request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
- return
- }
-
- // Give the fields on the request form a first pass to make sure the request is superficially valid.
- l.Tracef("validating form %+v", form)
- if err := validateCreateMedia(form, m.config.MediaConfig); err != nil {
- l.Debugf("error validating form: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- // open the attachment and extract the bytes from it
- f, err := form.File.Open()
- if err != nil {
- l.Debugf("error opening attachment: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not open provided attachment: %s", err)})
- return
- }
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, f)
- if err != nil {
- l.Debugf("error reading attachment: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not read provided attachment: %s", err)})
- return
- }
- if size == 0 {
- l.Debug("could not read provided attachment: size 0 bytes")
- c.JSON(http.StatusBadRequest, gin.H{"error": "could not read provided attachment: size 0 bytes"})
- return
- }
-
- // allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using
- attachment, err := m.mediaHandler.ProcessLocalAttachment(buf.Bytes(), authed.Account.ID)
- if err != nil {
- l.Debugf("error reading attachment: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not process attachment: %s", err)})
- return
- }
-
- // now we need to add extra fields that the attachment processor doesn't know (from the form)
- // TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it)
-
- // first description
- attachment.Description = form.Description
-
- // now parse the focus parameter
- // TODO: tidy this up into a separate function and just return an error so all the c.JSON and return calls are obviated
- var focusx, focusy float32
- if form.Focus != "" {
- spl := strings.Split(form.Focus, ",")
- if len(spl) != 2 {
- l.Debugf("improperly formatted focus %s", form.Focus)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
- return
- }
- xStr := spl[0]
- yStr := spl[1]
- if xStr == "" || yStr == "" {
- l.Debugf("improperly formatted focus %s", form.Focus)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
- return
- }
- fx, err := strconv.ParseFloat(xStr, 32)
- if err != nil {
- l.Debugf("improperly formatted focus %s: %s", form.Focus, err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
- return
- }
- if fx > 1 || fx < -1 {
- l.Debugf("improperly formatted focus %s", form.Focus)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
- return
- }
- focusx = float32(fx)
- fy, err := strconv.ParseFloat(yStr, 32)
- if err != nil {
- l.Debugf("improperly formatted focus %s: %s", form.Focus, err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
- return
- }
- if fy > 1 || fy < -1 {
- l.Debugf("improperly formatted focus %s", form.Focus)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
- return
- }
- focusy = float32(fy)
- }
- attachment.FileMeta.Focus.X = focusx
- attachment.FileMeta.Focus.Y = focusy
-
- // prepare the frontend representation now -- if there are any errors here at least we can bail without
- // having already put something in the database and then having to clean it up again (eugh)
- mastoAttachment, err := m.mastoConverter.AttachmentToMasto(attachment)
- if err != nil {
- l.Debugf("error parsing media attachment to frontend type: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("error parsing media attachment to frontend type: %s", err)})
- return
- }
-
- // now we can confidently put the attachment in the database
- if err := m.db.Put(attachment); err != nil {
- l.Debugf("error storing media attachment in db: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("error storing media attachment in db: %s", err)})
- return
- }
-
- // and return its frontend representation
- c.JSON(http.StatusAccepted, mastoAttachment)
-}
-
-func validateCreateMedia(form *mastotypes.AttachmentRequest, config *config.MediaConfig) error {
- // check there actually is a file attached and it's not size 0
- if form.File == nil || form.File.Size == 0 {
- return errors.New("no attachment given")
- }
-
- // a very superficial check to see if no size limits are exceeded
- // we still don't actually know which media types we're dealing with but the other handlers will go into more detail there
- maxSize := config.MaxVideoSize
- if config.MaxImageSize > maxSize {
- maxSize = config.MaxImageSize
- }
- if form.File.Size > int64(maxSize) {
- return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size)
- }
-
- if len(form.Description) < config.MinDescriptionChars || len(form.Description) > config.MaxDescriptionChars {
- return fmt.Errorf("image description length must be between %d and %d characters (inclusive), but provided image description was %d chars", config.MinDescriptionChars, config.MaxDescriptionChars, len(form.Description))
- }
-
- // TODO: validate focus here
-
- return nil
-}
diff --git a/internal/apimodule/media/test/mediacreate_test.go b/internal/apimodule/media/test/mediacreate_test.go
deleted file mode 100644
index 30bbb117a..000000000
--- a/internal/apimodule/media/test/mediacreate_test.go
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package test
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/suite"
- mediamodule "github.com/superseriousbusiness/gotosocial/internal/apimodule/media"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/testrig"
-)
-
-type MediaCreateTestSuite struct {
- // standard suite interfaces
- suite.Suite
- config *config.Config
- db db.DB
- log *logrus.Logger
- storage storage.Storage
- mastoConverter mastotypes.Converter
- mediaHandler media.Handler
- oauthServer oauth.Server
-
- // standard suite models
- testTokens map[string]*oauth.Token
- testClients map[string]*oauth.Client
- testApplications map[string]*gtsmodel.Application
- testUsers map[string]*gtsmodel.User
- testAccounts map[string]*gtsmodel.Account
- testAttachments map[string]*gtsmodel.MediaAttachment
-
- // item being tested
- mediaModule *mediamodule.Module
-}
-
-/*
- TEST INFRASTRUCTURE
-*/
-
-func (suite *MediaCreateTestSuite) SetupSuite() {
- // setup standard items
- suite.config = testrig.NewTestConfig()
- suite.db = testrig.NewTestDB()
- suite.log = testrig.NewTestLog()
- suite.storage = testrig.NewTestStorage()
- suite.mastoConverter = testrig.NewTestMastoConverter(suite.db)
- suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
-
- // setup module being tested
- suite.mediaModule = mediamodule.New(suite.db, suite.mediaHandler, suite.mastoConverter, suite.config, suite.log).(*mediamodule.Module)
-}
-
-func (suite *MediaCreateTestSuite) TearDownSuite() {
- if err := suite.db.Stop(context.Background()); err != nil {
- logrus.Panicf("error closing db connection: %s", err)
- }
-}
-
-func (suite *MediaCreateTestSuite) SetupTest() {
- testrig.StandardDBSetup(suite.db)
- testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
- suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
- suite.testApplications = testrig.NewTestApplications()
- suite.testUsers = testrig.NewTestUsers()
- suite.testAccounts = testrig.NewTestAccounts()
- suite.testAttachments = testrig.NewTestAttachments()
-}
-
-func (suite *MediaCreateTestSuite) TearDownTest() {
- testrig.StandardDBTeardown(suite.db)
- testrig.StandardStorageTeardown(suite.storage)
-}
-
-/*
- ACTUAL TESTS
-*/
-
-func (suite *MediaCreateTestSuite) TestStatusCreatePOSTImageHandlerSuccessful() {
-
- // set up the context for the request
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.TokenToOauthToken(t)
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
-
- // see what's in storage *before* the request
- storageKeysBeforeRequest, err := suite.storage.ListKeys()
- if err != nil {
- panic(err)
- }
-
- // create the request
- buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string]string{
- "description": "this is a test image -- a cool background from somewhere",
- "focus": "-0.5,0.5",
- })
- if err != nil {
- panic(err)
- }
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", mediamodule.BasePath), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting
- ctx.Request.Header.Set("Content-Type", w.FormDataContentType())
-
- // do the actual request
- suite.mediaModule.MediaCreatePOSTHandler(ctx)
-
- // check what's in storage *after* the request
- storageKeysAfterRequest, err := suite.storage.ListKeys()
- if err != nil {
- panic(err)
- }
-
- // check response
- suite.EqualValues(http.StatusAccepted, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- fmt.Println(string(b))
-
- attachmentReply := &mastomodel.Attachment{}
- err = json.Unmarshal(b, attachmentReply)
- assert.NoError(suite.T(), err)
-
- assert.Equal(suite.T(), "this is a test image -- a cool background from somewhere", attachmentReply.Description)
- assert.Equal(suite.T(), "image", attachmentReply.Type)
- assert.EqualValues(suite.T(), mastomodel.MediaMeta{
- Original: mastomodel.MediaDimensions{
- Width: 1920,
- Height: 1080,
- Size: "1920x1080",
- Aspect: 1.7777778,
- },
- Small: mastomodel.MediaDimensions{
- Width: 256,
- Height: 144,
- Size: "256x144",
- Aspect: 1.7777778,
- },
- Focus: mastomodel.MediaFocus{
- X: -0.5,
- Y: 0.5,
- },
- }, attachmentReply.Meta)
- assert.Equal(suite.T(), "LjCZnlvyRkRn_NvzRjWF?urqV@f9", attachmentReply.Blurhash)
- assert.NotEmpty(suite.T(), attachmentReply.ID)
- assert.NotEmpty(suite.T(), attachmentReply.URL)
- assert.NotEmpty(suite.T(), attachmentReply.PreviewURL)
- assert.Equal(suite.T(), len(storageKeysBeforeRequest)+2, len(storageKeysAfterRequest)) // 2 images should be added to storage: the original and the thumbnail
-}
-
-func TestMediaCreateTestSuite(t *testing.T) {
- suite.Run(t, new(MediaCreateTestSuite))
-}
diff --git a/internal/apimodule/mock_ClientAPIModule.go b/internal/apimodule/mock_ClientAPIModule.go
deleted file mode 100644
index 2d4293d0e..000000000
--- a/internal/apimodule/mock_ClientAPIModule.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Code generated by mockery v2.7.4. DO NOT EDIT.
-
-package apimodule
-
-import (
- mock "github.com/stretchr/testify/mock"
- db "github.com/superseriousbusiness/gotosocial/internal/db"
-
- router "github.com/superseriousbusiness/gotosocial/internal/router"
-)
-
-// MockClientAPIModule is an autogenerated mock type for the ClientAPIModule type
-type MockClientAPIModule struct {
- mock.Mock
-}
-
-// CreateTables provides a mock function with given fields: _a0
-func (_m *MockClientAPIModule) CreateTables(_a0 db.DB) error {
- ret := _m.Called(_a0)
-
- var r0 error
- if rf, ok := ret.Get(0).(func(db.DB) error); ok {
- r0 = rf(_a0)
- } else {
- r0 = ret.Error(0)
- }
-
- return r0
-}
-
-// Route provides a mock function with given fields: s
-func (_m *MockClientAPIModule) Route(s router.Router) error {
- ret := _m.Called(s)
-
- var r0 error
- if rf, ok := ret.Get(0).(func(router.Router) error); ok {
- r0 = rf(s)
- } else {
- r0 = ret.Error(0)
- }
-
- return r0
-}
diff --git a/internal/apimodule/security/flocblock.go b/internal/apimodule/security/flocblock.go
deleted file mode 100644
index 7cedcde6b..000000000
--- a/internal/apimodule/security/flocblock.go
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package security
-
-import "github.com/gin-gonic/gin"
-
-// FlocBlock is a middleware that prevents google chrome cohort tracking by
-// writing the Permissions-Policy header after all other parts of the request have been completed.
-// See: https://plausible.io/blog/google-floc
-func (m *Module) FlocBlock(c *gin.Context) {
- c.Header("Permissions-Policy", "interest-cohort=()")
-}
diff --git a/internal/apimodule/security/security.go b/internal/apimodule/security/security.go
deleted file mode 100644
index 8f805bc93..000000000
--- a/internal/apimodule/security/security.go
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package security
-
-import (
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/router"
-)
-
-// Module implements the ClientAPIModule interface for security middleware
-type Module struct {
- config *config.Config
- log *logrus.Logger
-}
-
-// New returns a new security module
-func New(config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule {
- return &Module{
- config: config,
- log: log,
- }
-}
-
-// Route attaches security middleware to the given router
-func (m *Module) Route(s router.Router) error {
- s.AttachMiddleware(m.FlocBlock)
- return nil
-}
-
-// CreateTables doesn't do diddly squat at the moment, it's just for fulfilling the interface
-func (m *Module) CreateTables(db db.DB) error {
- return nil
-}
diff --git a/internal/apimodule/status/status.go b/internal/apimodule/status/status.go
deleted file mode 100644
index 73a1b5847..000000000
--- a/internal/apimodule/status/status.go
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "fmt"
- "net/http"
- "strings"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/distributor"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/router"
-)
-
-const (
- // IDKey is for status UUIDs
- IDKey = "id"
- // BasePath is the base path for serving the status API
- BasePath = "/api/v1/statuses"
- // BasePathWithID is just the base path with the ID key in it.
- // Use this anywhere you need to know the ID of the status being queried.
- BasePathWithID = BasePath + "/:" + IDKey
-
- // ContextPath is used for fetching context of posts
- ContextPath = BasePathWithID + "/context"
-
- // FavouritedPath is for seeing who's faved a given status
- FavouritedPath = BasePathWithID + "/favourited_by"
- // FavouritePath is for posting a fave on a status
- FavouritePath = BasePathWithID + "/favourite"
- // UnfavouritePath is for removing a fave from a status
- UnfavouritePath = BasePathWithID + "/unfavourite"
-
- // RebloggedPath is for seeing who's boosted a given status
- RebloggedPath = BasePathWithID + "/reblogged_by"
- // ReblogPath is for boosting/reblogging a given status
- ReblogPath = BasePathWithID + "/reblog"
- // UnreblogPath is for undoing a boost/reblog of a given status
- UnreblogPath = BasePathWithID + "/unreblog"
-
- // BookmarkPath is for creating a bookmark on a given status
- BookmarkPath = BasePathWithID + "/bookmark"
- // UnbookmarkPath is for removing a bookmark from a given status
- UnbookmarkPath = BasePathWithID + "/unbookmark"
-
- // MutePath is for muting a given status so that notifications will no longer be received about it.
- MutePath = BasePathWithID + "/mute"
- // UnmutePath is for undoing an existing mute
- UnmutePath = BasePathWithID + "/unmute"
-
- // PinPath is for pinning a status to an account profile so that it's the first thing people see
- PinPath = BasePathWithID + "/pin"
- // UnpinPath is for undoing a pin and returning a status to the ever-swirling drain of time and entropy
- UnpinPath = BasePathWithID + "/unpin"
-)
-
-// Module implements the ClientAPIModule interface for every related to posting/deleting/interacting with statuses
-type Module struct {
- config *config.Config
- db db.DB
- mediaHandler media.Handler
- mastoConverter mastotypes.Converter
- distributor distributor.Distributor
- log *logrus.Logger
-}
-
-// New returns a new account module
-func New(config *config.Config, db db.DB, mediaHandler media.Handler, mastoConverter mastotypes.Converter, distributor distributor.Distributor, log *logrus.Logger) apimodule.ClientAPIModule {
- return &Module{
- config: config,
- db: db,
- mediaHandler: mediaHandler,
- mastoConverter: mastoConverter,
- distributor: distributor,
- log: log,
- }
-}
-
-// Route attaches all routes from this module to the given router
-func (m *Module) Route(r router.Router) error {
- r.AttachHandler(http.MethodPost, BasePath, m.StatusCreatePOSTHandler)
- r.AttachHandler(http.MethodDelete, BasePathWithID, m.StatusDELETEHandler)
-
- r.AttachHandler(http.MethodPost, FavouritePath, m.StatusFavePOSTHandler)
- r.AttachHandler(http.MethodPost, UnfavouritePath, m.StatusFavePOSTHandler)
-
- r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
- return nil
-}
-
-// CreateTables populates necessary tables in the given DB
-func (m *Module) CreateTables(db db.DB) error {
- models := []interface{}{
- &gtsmodel.User{},
- &gtsmodel.Account{},
- &gtsmodel.Block{},
- &gtsmodel.Follow{},
- &gtsmodel.FollowRequest{},
- &gtsmodel.Status{},
- &gtsmodel.StatusFave{},
- &gtsmodel.StatusBookmark{},
- &gtsmodel.StatusMute{},
- &gtsmodel.StatusPin{},
- &gtsmodel.Application{},
- &gtsmodel.EmailDomainBlock{},
- &gtsmodel.MediaAttachment{},
- &gtsmodel.Emoji{},
- &gtsmodel.Tag{},
- &gtsmodel.Mention{},
- }
-
- for _, m := range models {
- if err := db.CreateTable(m); err != nil {
- return fmt.Errorf("error creating table: %s", err)
- }
- }
- return nil
-}
-
-// muxHandler is a little workaround to overcome the limitations of Gin
-func (m *Module) muxHandler(c *gin.Context) {
- m.log.Debug("entering mux handler")
- ru := c.Request.RequestURI
-
- switch c.Request.Method {
- case http.MethodGet:
- if strings.HasPrefix(ru, ContextPath) {
- // TODO
- } else if strings.HasPrefix(ru, FavouritedPath) {
- m.StatusFavedByGETHandler(c)
- } else {
- m.StatusGETHandler(c)
- }
- }
-}
diff --git a/internal/apimodule/status/statuscreate.go b/internal/apimodule/status/statuscreate.go
deleted file mode 100644
index 97354e767..000000000
--- a/internal/apimodule/status/statuscreate.go
+++ /dev/null
@@ -1,462 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "errors"
- "fmt"
- "net/http"
- "time"
-
- "github.com/gin-gonic/gin"
- "github.com/google/uuid"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/distributor"
- mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-type advancedStatusCreateForm struct {
- mastotypes.StatusCreateRequest
- advancedVisibilityFlagsForm
-}
-
-type advancedVisibilityFlagsForm struct {
- // The gotosocial visibility model
- VisibilityAdvanced *gtsmodel.Visibility `form:"visibility_advanced"`
- // This status will be federated beyond the local timeline(s)
- Federated *bool `form:"federated"`
- // This status can be boosted/reblogged
- Boostable *bool `form:"boostable"`
- // This status can be replied to
- Replyable *bool `form:"replyable"`
- // This status can be liked/faved
- Likeable *bool `form:"likeable"`
-}
-
-// StatusCreatePOSTHandler deals with the creation of new statuses
-func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
- l := m.log.WithField("func", "statusCreatePOSTHandler")
- authed, err := oauth.MustAuth(c, true, true, true, true) // posting a status is serious business so we want *everything*
- if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
- return
- }
-
- // First check this user/account is permitted to post new statuses.
- // There's no point continuing otherwise.
- if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
- return
- }
-
- // extract the status create form from the request context
- l.Tracef("parsing request form: %s", c.Request.Form)
- form := &advancedStatusCreateForm{}
- if err := c.ShouldBind(form); err != nil || form == nil {
- l.Debugf("could not parse form from request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
- return
- }
-
- // Give the fields on the request form a first pass to make sure the request is superficially valid.
- l.Tracef("validating form %+v", form)
- if err := validateCreateStatus(form, m.config.StatusesConfig); err != nil {
- l.Debugf("error validating form: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- // At this point we know the account is permitted to post, and we know the request form
- // is valid (at least according to the API specifications and the instance configuration).
- // So now we can start digging a bit deeper into the form and building up the new status from it.
-
- // first we create a new status and add some basic info to it
- uris := util.GenerateURIs(authed.Account.Username, m.config.Protocol, m.config.Host)
- thisStatusID := uuid.NewString()
- thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID)
- thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID)
- newStatus := &gtsmodel.Status{
- ID: thisStatusID,
- URI: thisStatusURI,
- URL: thisStatusURL,
- Content: util.HTMLFormat(form.Status),
- CreatedAt: time.Now(),
- UpdatedAt: time.Now(),
- Local: true,
- AccountID: authed.Account.ID,
- ContentWarning: form.SpoilerText,
- ActivityStreamsType: gtsmodel.ActivityStreamsNote,
- Sensitive: form.Sensitive,
- Language: form.Language,
- CreatedWithApplicationID: authed.Application.ID,
- Text: form.Status,
- }
-
- // check if replyToID is ok
- if err := m.parseReplyToID(form, authed.Account.ID, newStatus); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- // check if mediaIDs are ok
- if err := m.parseMediaIDs(form, authed.Account.ID, newStatus); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- // check if visibility settings are ok
- if err := parseVisibility(form, authed.Account.Privacy, newStatus); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- // handle language settings
- if err := parseLanguage(form, authed.Account.Language, newStatus); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- // handle mentions
- if err := m.parseMentions(form, authed.Account.ID, newStatus); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- if err := m.parseTags(form, authed.Account.ID, newStatus); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- if err := m.parseEmojis(form, authed.Account.ID, newStatus); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- /*
- FROM THIS POINT ONWARDS WE ARE HAPPY WITH THE STATUS -- it is valid and we will try to create it
- */
-
- // put the new status in the database, generating an ID for it in the process
- if err := m.db.Put(newStatus); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- // change the status ID of the media attachments to the new status
- for _, a := range newStatus.GTSMediaAttachments {
- a.StatusID = newStatus.ID
- a.UpdatedAt = time.Now()
- if err := m.db.UpdateByID(a.ID, a); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- }
-
- // pass to the distributor to take care of side effects asynchronously -- federation, mentions, updating metadata, etc, etc
- m.distributor.FromClientAPI() <- distributor.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsNote,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- Activity: newStatus,
- }
-
- // return the frontend representation of the new status to the submitter
- mastoStatus, err := m.mastoConverter.StatusToMasto(newStatus, authed.Account, authed.Account, nil, newStatus.GTSReplyToAccount, nil)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, mastoStatus)
-}
-
-func validateCreateStatus(form *advancedStatusCreateForm, config *config.StatusesConfig) error {
- // validate that, structurally, we have a valid status/post
- if form.Status == "" && form.MediaIDs == nil && form.Poll == nil {
- return errors.New("no status, media, or poll provided")
- }
-
- if form.MediaIDs != nil && form.Poll != nil {
- return errors.New("can't post media + poll in same status")
- }
-
- // validate status
- if form.Status != "" {
- if len(form.Status) > config.MaxChars {
- return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), config.MaxChars)
- }
- }
-
- // validate media attachments
- if len(form.MediaIDs) > config.MaxMediaFiles {
- return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), config.MaxMediaFiles)
- }
-
- // validate poll
- if form.Poll != nil {
- if form.Poll.Options == nil {
- return errors.New("poll with no options")
- }
- if len(form.Poll.Options) > config.PollMaxOptions {
- return fmt.Errorf("too many poll options provided, %d provided but limit is %d", len(form.Poll.Options), config.PollMaxOptions)
- }
- for _, p := range form.Poll.Options {
- if len(p) > config.PollOptionMaxChars {
- return fmt.Errorf("poll option too long, %d characters provided but limit is %d", len(p), config.PollOptionMaxChars)
- }
- }
- }
-
- // validate spoiler text/cw
- if form.SpoilerText != "" {
- if len(form.SpoilerText) > config.CWMaxChars {
- return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), config.CWMaxChars)
- }
- }
-
- // validate post language
- if form.Language != "" {
- if err := util.ValidateLanguage(form.Language); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
- // by default all flags are set to true
- gtsAdvancedVis := &gtsmodel.VisibilityAdvanced{
- Federated: true,
- Boostable: true,
- Replyable: true,
- Likeable: true,
- }
-
- var gtsBasicVis gtsmodel.Visibility
- // Advanced takes priority if it's set.
- // If it's not set, take whatever masto visibility is set.
- // If *that's* not set either, then just take the account default.
- // If that's also not set, take the default for the whole instance.
- if form.VisibilityAdvanced != nil {
- gtsBasicVis = *form.VisibilityAdvanced
- } else if form.Visibility != "" {
- gtsBasicVis = util.ParseGTSVisFromMastoVis(form.Visibility)
- } else if accountDefaultVis != "" {
- gtsBasicVis = accountDefaultVis
- } else {
- gtsBasicVis = gtsmodel.VisibilityDefault
- }
-
- switch gtsBasicVis {
- case gtsmodel.VisibilityPublic:
- // for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
- break
- case gtsmodel.VisibilityUnlocked:
- // for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
- if form.Federated != nil {
- gtsAdvancedVis.Federated = *form.Federated
- }
-
- if form.Boostable != nil {
- gtsAdvancedVis.Boostable = *form.Boostable
- }
-
- if form.Replyable != nil {
- gtsAdvancedVis.Replyable = *form.Replyable
- }
-
- if form.Likeable != nil {
- gtsAdvancedVis.Likeable = *form.Likeable
- }
-
- case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
- // for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
- gtsAdvancedVis.Boostable = false
-
- if form.Federated != nil {
- gtsAdvancedVis.Federated = *form.Federated
- }
-
- if form.Replyable != nil {
- gtsAdvancedVis.Replyable = *form.Replyable
- }
-
- if form.Likeable != nil {
- gtsAdvancedVis.Likeable = *form.Likeable
- }
-
- case gtsmodel.VisibilityDirect:
- // direct is pretty easy: there's only one possible setting so return it
- gtsAdvancedVis.Federated = true
- gtsAdvancedVis.Boostable = false
- gtsAdvancedVis.Federated = true
- gtsAdvancedVis.Likeable = true
- }
-
- status.Visibility = gtsBasicVis
- status.VisibilityAdvanced = gtsAdvancedVis
- return nil
-}
-
-func (m *Module) parseReplyToID(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
- if form.InReplyToID == "" {
- return nil
- }
-
- // If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted:
- //
- // 1. Does the replied status exist in the database?
- // 2. Is the replied status marked as replyable?
- // 3. Does a block exist between either the current account or the account that posted the status it's replying to?
- //
- // If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
- repliedStatus := &gtsmodel.Status{}
- repliedAccount := &gtsmodel.Account{}
- // check replied status exists + is replyable
- if err := m.db.GetByID(form.InReplyToID, repliedStatus); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
- }
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
- }
-
- if !repliedStatus.VisibilityAdvanced.Replyable {
- return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
- }
-
- // check replied account is known to us
- if err := m.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
- }
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
- }
- // check if a block exists
- if blocked, err := m.db.Blocked(thisAccountID, repliedAccount.ID); err != nil {
- if _, ok := err.(db.ErrNoEntries); !ok {
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
- }
- } else if blocked {
- return fmt.Errorf("status with id %s not replyable", form.InReplyToID)
- }
- status.InReplyToID = repliedStatus.ID
- status.InReplyToAccountID = repliedAccount.ID
-
- return nil
-}
-
-func (m *Module) parseMediaIDs(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
- if form.MediaIDs == nil {
- return nil
- }
-
- gtsMediaAttachments := []*gtsmodel.MediaAttachment{}
- attachments := []string{}
- for _, mediaID := range form.MediaIDs {
- // check these attachments exist
- a := &gtsmodel.MediaAttachment{}
- if err := m.db.GetByID(mediaID, a); err != nil {
- return fmt.Errorf("invalid media type or media not found for media id %s", mediaID)
- }
- // check they belong to the requesting account id
- if a.AccountID != thisAccountID {
- return fmt.Errorf("media with id %s does not belong to account %s", mediaID, thisAccountID)
- }
- // check they're not already used in a status
- if a.StatusID != "" || a.ScheduledStatusID != "" {
- return fmt.Errorf("media with id %s is already attached to a status", mediaID)
- }
- gtsMediaAttachments = append(gtsMediaAttachments, a)
- attachments = append(attachments, a.ID)
- }
- status.GTSMediaAttachments = gtsMediaAttachments
- status.Attachments = attachments
- return nil
-}
-
-func parseLanguage(form *advancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
- if form.Language != "" {
- status.Language = form.Language
- } else {
- status.Language = accountDefaultLanguage
- }
- if status.Language == "" {
- return errors.New("no language given either in status create form or account default")
- }
- return nil
-}
-
-func (m *Module) parseMentions(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- menchies := []string{}
- gtsMenchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), accountID, status.ID)
- if err != nil {
- return fmt.Errorf("error generating mentions from status: %s", err)
- }
- for _, menchie := range gtsMenchies {
- if err := m.db.Put(menchie); err != nil {
- return fmt.Errorf("error putting mentions in db: %s", err)
- }
- menchies = append(menchies, menchie.TargetAccountID)
- }
- // add full populated gts menchies to the status for passing them around conveniently
- status.GTSMentions = gtsMenchies
- // add just the ids of the mentioned accounts to the status for putting in the db
- status.Mentions = menchies
- return nil
-}
-
-func (m *Module) parseTags(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- tags := []string{}
- gtsTags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), accountID, status.ID)
- if err != nil {
- return fmt.Errorf("error generating hashtags from status: %s", err)
- }
- for _, tag := range gtsTags {
- if err := m.db.Upsert(tag, "name"); err != nil {
- return fmt.Errorf("error putting tags in db: %s", err)
- }
- tags = append(tags, tag.ID)
- }
- // add full populated gts tags to the status for passing them around conveniently
- status.GTSTags = gtsTags
- // add just the ids of the used tags to the status for putting in the db
- status.Tags = tags
- return nil
-}
-
-func (m *Module) parseEmojis(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- emojis := []string{}
- gtsEmojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), accountID, status.ID)
- if err != nil {
- return fmt.Errorf("error generating emojis from status: %s", err)
- }
- for _, e := range gtsEmojis {
- emojis = append(emojis, e.ID)
- }
- // add full populated gts emojis to the status for passing them around conveniently
- status.GTSEmojis = gtsEmojis
- // add just the ids of the used emojis to the status for putting in the db
- status.Emojis = emojis
- return nil
-}
diff --git a/internal/apimodule/status/statusdelete.go b/internal/apimodule/status/statusdelete.go
deleted file mode 100644
index 01dfe81df..000000000
--- a/internal/apimodule/status/statusdelete.go
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "fmt"
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/distributor"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-// StatusDELETEHandler verifies and handles deletion of a status
-func (m *Module) StatusDELETEHandler(c *gin.Context) {
- l := m.log.WithFields(logrus.Fields{
- "func": "StatusDELETEHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
- authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else
- if err != nil {
- l.Debug("not authed so can't delete status")
- c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
- return
- }
-
- targetStatusID := c.Param(IDKey)
- if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
- return
- }
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := &gtsmodel.Status{}
- if err := m.db.GetByID(targetStatusID, targetStatus); err != nil {
- l.Errorf("error fetching status %s: %s", targetStatusID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- if targetStatus.AccountID != authed.Account.ID {
- l.Debug("status doesn't belong to requesting account")
- c.JSON(http.StatusForbidden, gin.H{"error": "not allowed"})
- return
- }
-
- l.Trace("going to get relevant accounts")
- relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus)
- if err != nil {
- l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- var boostOfStatus *gtsmodel.Status
- if targetStatus.BoostOfID != "" {
- boostOfStatus = &gtsmodel.Status{}
- if err := m.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
- l.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
- }
-
- mastoStatus, err := m.mastoConverter.StatusToMasto(targetStatus, authed.Account, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
- if err != nil {
- l.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- if err := m.db.DeleteByID(targetStatus.ID, targetStatus); err != nil {
- l.Errorf("error deleting status from the database: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- m.distributor.FromClientAPI() <- distributor.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsNote,
- APActivityType: gtsmodel.ActivityStreamsDelete,
- Activity: targetStatus,
- }
-
- c.JSON(http.StatusOK, mastoStatus)
-}
diff --git a/internal/apimodule/status/statusfave.go b/internal/apimodule/status/statusfave.go
deleted file mode 100644
index 9ce68af09..000000000
--- a/internal/apimodule/status/statusfave.go
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "fmt"
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/distributor"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-// StatusFavePOSTHandler handles fave requests against a given status ID
-func (m *Module) StatusFavePOSTHandler(c *gin.Context) {
- l := m.log.WithFields(logrus.Fields{
- "func": "StatusFavePOSTHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
- authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else
- if err != nil {
- l.Debug("not authed so can't fave status")
- c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
- return
- }
-
- targetStatusID := c.Param(IDKey)
- if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
- return
- }
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := &gtsmodel.Status{}
- if err := m.db.GetByID(targetStatusID, targetStatus); err != nil {
- l.Errorf("error fetching status %s: %s", targetStatusID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := &gtsmodel.Account{}
- if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Trace("going to get relevant accounts")
- relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus)
- if err != nil {
- l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Trace("going to see if status is visible")
- visible, err := m.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
- if err != nil {
- l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- if !visible {
- l.Trace("status is not visible")
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- // is the status faveable?
- if !targetStatus.VisibilityAdvanced.Likeable {
- l.Debug("status is not faveable")
- c.JSON(http.StatusForbidden, gin.H{"error": fmt.Sprintf("status %s not faveable", targetStatusID)})
- return
- }
-
- // it's visible! it's faveable! so let's fave the FUCK out of it
- fave, err := m.db.FaveStatus(targetStatus, authed.Account.ID)
- if err != nil {
- l.Debugf("error faveing status: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- var boostOfStatus *gtsmodel.Status
- if targetStatus.BoostOfID != "" {
- boostOfStatus = &gtsmodel.Status{}
- if err := m.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
- l.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
- }
-
- mastoStatus, err := m.mastoConverter.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
- if err != nil {
- l.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- // if the targeted status was already faved, faved will be nil
- // only put the fave in the distributor if something actually changed
- if fave != nil {
- fave.FavedStatus = targetStatus // attach the status pointer to the fave for easy retrieval in the distributor
- m.distributor.FromClientAPI() <- distributor.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsNote, // status is a note
- APActivityType: gtsmodel.ActivityStreamsLike, // we're creating a like/fave on the note
- Activity: fave, // pass the fave along for processing
- }
- }
-
- c.JSON(http.StatusOK, mastoStatus)
-}
diff --git a/internal/apimodule/status/statusfavedby.go b/internal/apimodule/status/statusfavedby.go
deleted file mode 100644
index 58236edc2..000000000
--- a/internal/apimodule/status/statusfavedby.go
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "fmt"
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-// StatusFavedByGETHandler is for serving a list of accounts that have faved a given status
-func (m *Module) StatusFavedByGETHandler(c *gin.Context) {
- l := m.log.WithFields(logrus.Fields{
- "func": "statusGETHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
- var requestingAccount *gtsmodel.Account
- authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else
- if err != nil {
- l.Debug("not authed but will continue to serve anyway if public status")
- requestingAccount = nil
- } else {
- requestingAccount = authed.Account
- }
-
- targetStatusID := c.Param(IDKey)
- if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
- return
- }
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := &gtsmodel.Status{}
- if err := m.db.GetByID(targetStatusID, targetStatus); err != nil {
- l.Errorf("error fetching status %s: %s", targetStatusID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := &gtsmodel.Account{}
- if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Trace("going to get relevant accounts")
- relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus)
- if err != nil {
- l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Trace("going to see if status is visible")
- visible, err := m.db.StatusVisible(targetStatus, targetAccount, requestingAccount, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
- if err != nil {
- l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- if !visible {
- l.Trace("status is not visible")
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- // get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
- favingAccounts, err := m.db.WhoFavedStatus(targetStatus)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- // filter the list so the user doesn't see accounts they blocked or which blocked them
- filteredAccounts := []*gtsmodel.Account{}
- for _, acc := range favingAccounts {
- blocked, err := m.db.Blocked(authed.Account.ID, acc.ID)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- if !blocked {
- filteredAccounts = append(filteredAccounts, acc)
- }
- }
-
- // TODO: filter other things here? suspended? muted? silenced?
-
- // now we can return the masto representation of those accounts
- mastoAccounts := []*mastotypes.Account{}
- for _, acc := range filteredAccounts {
- mastoAccount, err := m.mastoConverter.AccountToMastoPublic(acc)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- mastoAccounts = append(mastoAccounts, mastoAccount)
- }
-
- c.JSON(http.StatusOK, mastoAccounts)
-}
diff --git a/internal/apimodule/status/statusget.go b/internal/apimodule/status/statusget.go
deleted file mode 100644
index 76918c782..000000000
--- a/internal/apimodule/status/statusget.go
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "fmt"
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-// StatusGETHandler is for handling requests to just get one status based on its ID
-func (m *Module) StatusGETHandler(c *gin.Context) {
- l := m.log.WithFields(logrus.Fields{
- "func": "statusGETHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
- var requestingAccount *gtsmodel.Account
- authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else
- if err != nil {
- l.Debug("not authed but will continue to serve anyway if public status")
- requestingAccount = nil
- } else {
- requestingAccount = authed.Account
- }
-
- targetStatusID := c.Param(IDKey)
- if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
- return
- }
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := &gtsmodel.Status{}
- if err := m.db.GetByID(targetStatusID, targetStatus); err != nil {
- l.Errorf("error fetching status %s: %s", targetStatusID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := &gtsmodel.Account{}
- if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Trace("going to get relevant accounts")
- relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus)
- if err != nil {
- l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Trace("going to see if status is visible")
- visible, err := m.db.StatusVisible(targetStatus, targetAccount, requestingAccount, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
- if err != nil {
- l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- if !visible {
- l.Trace("status is not visible")
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- var boostOfStatus *gtsmodel.Status
- if targetStatus.BoostOfID != "" {
- boostOfStatus = &gtsmodel.Status{}
- if err := m.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
- l.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
- }
-
- mastoStatus, err := m.mastoConverter.StatusToMasto(targetStatus, targetAccount, requestingAccount, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
- if err != nil {
- l.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- c.JSON(http.StatusOK, mastoStatus)
-}
diff --git a/internal/apimodule/status/statusunfave.go b/internal/apimodule/status/statusunfave.go
deleted file mode 100644
index 9c06eaf92..000000000
--- a/internal/apimodule/status/statusunfave.go
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "fmt"
- "net/http"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/distributor"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-// StatusUnfavePOSTHandler is for undoing a fave on a status with a given ID
-func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) {
- l := m.log.WithFields(logrus.Fields{
- "func": "StatusUnfavePOSTHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
- authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else
- if err != nil {
- l.Debug("not authed so can't unfave status")
- c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
- return
- }
-
- targetStatusID := c.Param(IDKey)
- if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
- return
- }
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := &gtsmodel.Status{}
- if err := m.db.GetByID(targetStatusID, targetStatus); err != nil {
- l.Errorf("error fetching status %s: %s", targetStatusID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := &gtsmodel.Account{}
- if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Trace("going to get relevant accounts")
- relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus)
- if err != nil {
- l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- l.Trace("going to see if status is visible")
- visible, err := m.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
- if err != nil {
- l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- if !visible {
- l.Trace("status is not visible")
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- // is the status faveable?
- if !targetStatus.VisibilityAdvanced.Likeable {
- l.Debug("status is not faveable")
- c.JSON(http.StatusForbidden, gin.H{"error": fmt.Sprintf("status %s not faveable so therefore not unfave-able", targetStatusID)})
- return
- }
-
- // it's visible! it's faveable! so let's unfave the FUCK out of it
- fave, err := m.db.UnfaveStatus(targetStatus, authed.Account.ID)
- if err != nil {
- l.Debugf("error unfaveing status: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
-
- var boostOfStatus *gtsmodel.Status
- if targetStatus.BoostOfID != "" {
- boostOfStatus = &gtsmodel.Status{}
- if err := m.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
- l.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
- }
-
- mastoStatus, err := m.mastoConverter.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
- if err != nil {
- l.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
- c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
- return
- }
-
- // fave might be nil if this status wasn't faved in the first place
- // we only want to pass the message to the distributor if something actually changed
- if fave != nil {
- fave.FavedStatus = targetStatus // attach the status pointer to the fave for easy retrieval in the distributor
- m.distributor.FromClientAPI() <- distributor.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsNote, // status is a note
- APActivityType: gtsmodel.ActivityStreamsUndo, // undo the fave
- Activity: fave, // pass the undone fave along
- }
- }
-
- c.JSON(http.StatusOK, mastoStatus)
-}
diff --git a/internal/apimodule/status/test/statuscreate_test.go b/internal/apimodule/status/test/statuscreate_test.go
deleted file mode 100644
index d143ac9a7..000000000
--- a/internal/apimodule/status/test/statuscreate_test.go
+++ /dev/null
@@ -1,346 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "net/url"
- "testing"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule/status"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/distributor"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/testrig"
-)
-
-type StatusCreateTestSuite struct {
- // standard suite interfaces
- suite.Suite
- config *config.Config
- db db.DB
- log *logrus.Logger
- storage storage.Storage
- mastoConverter mastotypes.Converter
- mediaHandler media.Handler
- oauthServer oauth.Server
- distributor distributor.Distributor
-
- // standard suite models
- testTokens map[string]*oauth.Token
- testClients map[string]*oauth.Client
- testApplications map[string]*gtsmodel.Application
- testUsers map[string]*gtsmodel.User
- testAccounts map[string]*gtsmodel.Account
- testAttachments map[string]*gtsmodel.MediaAttachment
-
- // module being tested
- statusModule *status.Module
-}
-
-/*
- TEST INFRASTRUCTURE
-*/
-
-// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
-func (suite *StatusCreateTestSuite) SetupSuite() {
- // setup standard items
- suite.config = testrig.NewTestConfig()
- suite.db = testrig.NewTestDB()
- suite.log = testrig.NewTestLog()
- suite.storage = testrig.NewTestStorage()
- suite.mastoConverter = testrig.NewTestMastoConverter(suite.db)
- suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
- suite.distributor = testrig.NewTestDistributor()
-
- // setup module being tested
- suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module)
-}
-
-func (suite *StatusCreateTestSuite) TearDownSuite() {
- testrig.StandardDBTeardown(suite.db)
- testrig.StandardStorageTeardown(suite.storage)
-}
-
-func (suite *StatusCreateTestSuite) SetupTest() {
- testrig.StandardDBSetup(suite.db)
- testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
- suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
- suite.testApplications = testrig.NewTestApplications()
- suite.testUsers = testrig.NewTestUsers()
- suite.testAccounts = testrig.NewTestAccounts()
- suite.testAttachments = testrig.NewTestAttachments()
-}
-
-// TearDownTest drops tables to make sure there's no data in the db
-func (suite *StatusCreateTestSuite) TearDownTest() {
- testrig.StandardDBTeardown(suite.db)
-}
-
-/*
- ACTUAL TESTS
-*/
-
-/*
- TESTING: StatusCreatePOSTHandler
-*/
-
-// Post a new status with some custom visibility settings
-func (suite *StatusCreateTestSuite) TestPostNewStatus() {
-
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.TokenToOauthToken(t)
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = url.Values{
- "status": {"this is a brand new status! #helloworld"},
- "spoiler_text": {"hello hello"},
- "sensitive": {"true"},
- "visibility_advanced": {"mutuals_only"},
- "likeable": {"false"},
- "replyable": {"false"},
- "federated": {"false"},
- }
- suite.statusModule.StatusCreatePOSTHandler(ctx)
-
- // check response
-
- // 1. we should have OK from our call to the function
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
-
- statusReply := &mastomodel.Status{}
- err = json.Unmarshal(b, statusReply)
- assert.NoError(suite.T(), err)
-
- assert.Equal(suite.T(), "hello hello", statusReply.SpoilerText)
- assert.Equal(suite.T(), "this is a brand new status! #helloworld", statusReply.Content)
- assert.True(suite.T(), statusReply.Sensitive)
- assert.Equal(suite.T(), mastomodel.VisibilityPrivate, statusReply.Visibility)
- assert.Len(suite.T(), statusReply.Tags, 1)
- assert.Equal(suite.T(), mastomodel.Tag{
- Name: "helloworld",
- URL: "http://localhost:8080/tags/helloworld",
- }, statusReply.Tags[0])
-
- gtsTag := &gtsmodel.Tag{}
- err = suite.db.GetWhere("name", "helloworld", gtsTag)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), statusReply.Account.ID, gtsTag.FirstSeenFromAccountID)
-}
-
-func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() {
-
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.TokenToOauthToken(t)
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = url.Values{
- "status": {"here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: "},
- }
- suite.statusModule.StatusCreatePOSTHandler(ctx)
-
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
-
- statusReply := &mastomodel.Status{}
- err = json.Unmarshal(b, statusReply)
- assert.NoError(suite.T(), err)
-
- assert.Equal(suite.T(), "", statusReply.SpoilerText)
- assert.Equal(suite.T(), "here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: ", statusReply.Content)
-
- assert.Len(suite.T(), statusReply.Emojis, 1)
- mastoEmoji := statusReply.Emojis[0]
- gtsEmoji := testrig.NewTestEmojis()["rainbow"]
-
- assert.Equal(suite.T(), gtsEmoji.Shortcode, mastoEmoji.Shortcode)
- assert.Equal(suite.T(), gtsEmoji.ImageURL, mastoEmoji.URL)
- assert.Equal(suite.T(), gtsEmoji.ImageStaticURL, mastoEmoji.StaticURL)
-}
-
-// Try to reply to a status that doesn't exist
-func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.TokenToOauthToken(t)
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = url.Values{
- "status": {"this is a reply to a status that doesn't exist"},
- "spoiler_text": {"don't open cuz it won't work"},
- "in_reply_to_id": {"3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50"},
- }
- suite.statusModule.StatusCreatePOSTHandler(ctx)
-
- // check response
-
- suite.EqualValues(http.StatusBadRequest, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), `{"error":"status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b))
-}
-
-// Post a reply to the status of a local user that allows replies.
-func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() {
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.TokenToOauthToken(t)
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = url.Values{
- "status": {fmt.Sprintf("hello @%s this reply should work!", testrig.NewTestAccounts()["local_account_2"].Username)},
- "in_reply_to_id": {testrig.NewTestStatuses()["local_account_2_status_1"].ID},
- }
- suite.statusModule.StatusCreatePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
-
- statusReply := &mastomodel.Status{}
- err = json.Unmarshal(b, statusReply)
- assert.NoError(suite.T(), err)
-
- assert.Equal(suite.T(), "", statusReply.SpoilerText)
- assert.Equal(suite.T(), fmt.Sprintf("hello @%s this reply should work!", testrig.NewTestAccounts()["local_account_2"].Username), statusReply.Content)
- assert.False(suite.T(), statusReply.Sensitive)
- assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility)
- assert.Equal(suite.T(), testrig.NewTestStatuses()["local_account_2_status_1"].ID, statusReply.InReplyToID)
- assert.Equal(suite.T(), testrig.NewTestAccounts()["local_account_2"].ID, statusReply.InReplyToAccountID)
- assert.Len(suite.T(), statusReply.Mentions, 1)
-}
-
-// Take a media file which is currently not associated with a status, and attach it to a new status.
-func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() {
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.TokenToOauthToken(t)
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Form = url.Values{
- "status": {"here's an image attachment"},
- "media_ids": {"7a3b9f77-ab30-461e-bdd8-e64bd1db3008"},
- }
- suite.statusModule.StatusCreatePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
-
- fmt.Println(string(b))
-
- statusReply := &mastomodel.Status{}
- err = json.Unmarshal(b, statusReply)
- assert.NoError(suite.T(), err)
-
- assert.Equal(suite.T(), "", statusReply.SpoilerText)
- assert.Equal(suite.T(), "here's an image attachment", statusReply.Content)
- assert.False(suite.T(), statusReply.Sensitive)
- assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility)
-
- // there should be one media attachment
- assert.Len(suite.T(), statusReply.MediaAttachments, 1)
-
- // get the updated media attachment from the database
- gtsAttachment := &gtsmodel.MediaAttachment{}
- err = suite.db.GetByID(statusReply.MediaAttachments[0].ID, gtsAttachment)
- assert.NoError(suite.T(), err)
-
- // convert it to a masto attachment
- gtsAttachmentAsMasto, err := suite.mastoConverter.AttachmentToMasto(gtsAttachment)
- assert.NoError(suite.T(), err)
-
- // compare it with what we have now
- assert.EqualValues(suite.T(), statusReply.MediaAttachments[0], gtsAttachmentAsMasto)
-
- // the status id of the attachment should now be set to the id of the status we just created
- assert.Equal(suite.T(), statusReply.ID, gtsAttachment.StatusID)
-}
-
-func TestStatusCreateTestSuite(t *testing.T) {
- suite.Run(t, new(StatusCreateTestSuite))
-}
diff --git a/internal/apimodule/status/test/statusfave_test.go b/internal/apimodule/status/test/statusfave_test.go
deleted file mode 100644
index 9ccf58948..000000000
--- a/internal/apimodule/status/test/statusfave_test.go
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule/status"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/distributor"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/testrig"
-)
-
-type StatusFaveTestSuite struct {
- // standard suite interfaces
- suite.Suite
- config *config.Config
- db db.DB
- log *logrus.Logger
- storage storage.Storage
- mastoConverter mastotypes.Converter
- mediaHandler media.Handler
- oauthServer oauth.Server
- distributor distributor.Distributor
-
- // standard suite models
- testTokens map[string]*oauth.Token
- testClients map[string]*oauth.Client
- testApplications map[string]*gtsmodel.Application
- testUsers map[string]*gtsmodel.User
- testAccounts map[string]*gtsmodel.Account
- testAttachments map[string]*gtsmodel.MediaAttachment
- testStatuses map[string]*gtsmodel.Status
-
- // module being tested
- statusModule *status.Module
-}
-
-/*
- TEST INFRASTRUCTURE
-*/
-
-// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
-func (suite *StatusFaveTestSuite) SetupSuite() {
- // setup standard items
- suite.config = testrig.NewTestConfig()
- suite.db = testrig.NewTestDB()
- suite.log = testrig.NewTestLog()
- suite.storage = testrig.NewTestStorage()
- suite.mastoConverter = testrig.NewTestMastoConverter(suite.db)
- suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
- suite.distributor = testrig.NewTestDistributor()
-
- // setup module being tested
- suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module)
-}
-
-func (suite *StatusFaveTestSuite) TearDownSuite() {
- testrig.StandardDBTeardown(suite.db)
- testrig.StandardStorageTeardown(suite.storage)
-}
-
-func (suite *StatusFaveTestSuite) SetupTest() {
- testrig.StandardDBSetup(suite.db)
- testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
- suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
- suite.testApplications = testrig.NewTestApplications()
- suite.testUsers = testrig.NewTestUsers()
- suite.testAccounts = testrig.NewTestAccounts()
- suite.testAttachments = testrig.NewTestAttachments()
- suite.testStatuses = testrig.NewTestStatuses()
-}
-
-// TearDownTest drops tables to make sure there's no data in the db
-func (suite *StatusFaveTestSuite) TearDownTest() {
- testrig.StandardDBTeardown(suite.db)
- testrig.StandardStorageTeardown(suite.storage)
-}
-
-/*
- ACTUAL TESTS
-*/
-
-// fave a status
-func (suite *StatusFaveTestSuite) TestPostFave() {
-
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.TokenToOauthToken(t)
-
- targetStatus := suite.testStatuses["admin_account_status_2"]
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
-
- // normally the router would populate these params from the path values,
- // but because we're calling the function directly, we need to set them manually.
- ctx.Params = gin.Params{
- gin.Param{
- Key: status.IDKey,
- Value: targetStatus.ID,
- },
- }
-
- suite.statusModule.StatusFavePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
-
- statusReply := &mastomodel.Status{}
- err = json.Unmarshal(b, statusReply)
- assert.NoError(suite.T(), err)
-
- assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText)
- assert.Equal(suite.T(), targetStatus.Content, statusReply.Content)
- assert.True(suite.T(), statusReply.Sensitive)
- assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility)
- assert.True(suite.T(), statusReply.Favourited)
- assert.Equal(suite.T(), 1, statusReply.FavouritesCount)
-}
-
-// try to fave a status that's not faveable
-func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
-
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.TokenToOauthToken(t)
-
- targetStatus := suite.testStatuses["local_account_2_status_3"] // this one is unlikeable and unreplyable
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
-
- // normally the router would populate these params from the path values,
- // but because we're calling the function directly, we need to set them manually.
- ctx.Params = gin.Params{
- gin.Param{
- Key: status.IDKey,
- Value: targetStatus.ID,
- },
- }
-
- suite.statusModule.StatusFavePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusForbidden, recorder.Code) // we 403 unlikeable statuses
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), fmt.Sprintf(`{"error":"status %s not faveable"}`, targetStatus.ID), string(b))
-}
-
-func TestStatusFaveTestSuite(t *testing.T) {
- suite.Run(t, new(StatusFaveTestSuite))
-}
diff --git a/internal/apimodule/status/test/statusfavedby_test.go b/internal/apimodule/status/test/statusfavedby_test.go
deleted file mode 100644
index 169543a81..000000000
--- a/internal/apimodule/status/test/statusfavedby_test.go
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule/status"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/distributor"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/testrig"
-)
-
-type StatusFavedByTestSuite struct {
- // standard suite interfaces
- suite.Suite
- config *config.Config
- db db.DB
- log *logrus.Logger
- storage storage.Storage
- mastoConverter mastotypes.Converter
- mediaHandler media.Handler
- oauthServer oauth.Server
- distributor distributor.Distributor
-
- // standard suite models
- testTokens map[string]*oauth.Token
- testClients map[string]*oauth.Client
- testApplications map[string]*gtsmodel.Application
- testUsers map[string]*gtsmodel.User
- testAccounts map[string]*gtsmodel.Account
- testAttachments map[string]*gtsmodel.MediaAttachment
- testStatuses map[string]*gtsmodel.Status
-
- // module being tested
- statusModule *status.Module
-}
-
-// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
-func (suite *StatusFavedByTestSuite) SetupSuite() {
- // setup standard items
- suite.config = testrig.NewTestConfig()
- suite.db = testrig.NewTestDB()
- suite.log = testrig.NewTestLog()
- suite.storage = testrig.NewTestStorage()
- suite.mastoConverter = testrig.NewTestMastoConverter(suite.db)
- suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
- suite.distributor = testrig.NewTestDistributor()
-
- // setup module being tested
- suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module)
-}
-
-func (suite *StatusFavedByTestSuite) TearDownSuite() {
- testrig.StandardDBTeardown(suite.db)
- testrig.StandardStorageTeardown(suite.storage)
-}
-
-func (suite *StatusFavedByTestSuite) SetupTest() {
- testrig.StandardDBSetup(suite.db)
- testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
- suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
- suite.testApplications = testrig.NewTestApplications()
- suite.testUsers = testrig.NewTestUsers()
- suite.testAccounts = testrig.NewTestAccounts()
- suite.testAttachments = testrig.NewTestAttachments()
- suite.testStatuses = testrig.NewTestStatuses()
-}
-
-// TearDownTest drops tables to make sure there's no data in the db
-func (suite *StatusFavedByTestSuite) TearDownTest() {
- testrig.StandardDBTeardown(suite.db)
- testrig.StandardStorageTeardown(suite.storage)
-}
-
-/*
- ACTUAL TESTS
-*/
-
-func (suite *StatusFavedByTestSuite) TestGetFavedBy() {
- t := suite.testTokens["local_account_2"]
- oauthToken := oauth.TokenToOauthToken(t)
-
- targetStatus := suite.testStatuses["admin_account_status_1"] // this status is faved by local_account_1
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_2"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_2"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_2"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.FavouritedPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
-
- // normally the router would populate these params from the path values,
- // but because we're calling the function directly, we need to set them manually.
- ctx.Params = gin.Params{
- gin.Param{
- Key: status.IDKey,
- Value: targetStatus.ID,
- },
- }
-
- suite.statusModule.StatusFavedByGETHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
-
- accts := []mastomodel.Account{}
- err = json.Unmarshal(b, &accts)
- assert.NoError(suite.T(), err)
-
- assert.Len(suite.T(), accts, 1)
- assert.Equal(suite.T(), "the_mighty_zork", accts[0].Username)
-}
-
-func TestStatusFavedByTestSuite(t *testing.T) {
- suite.Run(t, new(StatusFavedByTestSuite))
-}
diff --git a/internal/apimodule/status/test/statusget_test.go b/internal/apimodule/status/test/statusget_test.go
deleted file mode 100644
index ce817d247..000000000
--- a/internal/apimodule/status/test/statusget_test.go
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "testing"
-
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule/status"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/distributor"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/testrig"
-)
-
-type StatusGetTestSuite struct {
- // standard suite interfaces
- suite.Suite
- config *config.Config
- db db.DB
- log *logrus.Logger
- storage storage.Storage
- mastoConverter mastotypes.Converter
- mediaHandler media.Handler
- oauthServer oauth.Server
- distributor distributor.Distributor
-
- // standard suite models
- testTokens map[string]*oauth.Token
- testClients map[string]*oauth.Client
- testApplications map[string]*gtsmodel.Application
- testUsers map[string]*gtsmodel.User
- testAccounts map[string]*gtsmodel.Account
- testAttachments map[string]*gtsmodel.MediaAttachment
-
- // module being tested
- statusModule *status.Module
-}
-
-/*
- TEST INFRASTRUCTURE
-*/
-
-// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
-func (suite *StatusGetTestSuite) SetupSuite() {
- // setup standard items
- suite.config = testrig.NewTestConfig()
- suite.db = testrig.NewTestDB()
- suite.log = testrig.NewTestLog()
- suite.storage = testrig.NewTestStorage()
- suite.mastoConverter = testrig.NewTestMastoConverter(suite.db)
- suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
- suite.distributor = testrig.NewTestDistributor()
-
- // setup module being tested
- suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module)
-}
-
-func (suite *StatusGetTestSuite) TearDownSuite() {
- testrig.StandardDBTeardown(suite.db)
- testrig.StandardStorageTeardown(suite.storage)
-}
-
-func (suite *StatusGetTestSuite) SetupTest() {
- testrig.StandardDBSetup(suite.db)
- testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
- suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
- suite.testApplications = testrig.NewTestApplications()
- suite.testUsers = testrig.NewTestUsers()
- suite.testAccounts = testrig.NewTestAccounts()
- suite.testAttachments = testrig.NewTestAttachments()
-}
-
-// TearDownTest drops tables to make sure there's no data in the db
-func (suite *StatusGetTestSuite) TearDownTest() {
- testrig.StandardDBTeardown(suite.db)
-}
-
-/*
- ACTUAL TESTS
-*/
-
-/*
- TESTING: StatusGetPOSTHandler
-*/
-
-// Post a new status with some custom visibility settings
-func (suite *StatusGetTestSuite) TestPostNewStatus() {
-
- // t := suite.testTokens["local_account_1"]
- // oauthToken := oauth.PGTokenToOauthToken(t)
-
- // // setup
- // recorder := httptest.NewRecorder()
- // ctx, _ := gin.CreateTestContext(recorder)
- // ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- // ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- // ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- // ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- // ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting
- // ctx.Request.Form = url.Values{
- // "status": {"this is a brand new status! #helloworld"},
- // "spoiler_text": {"hello hello"},
- // "sensitive": {"true"},
- // "visibility_advanced": {"mutuals_only"},
- // "likeable": {"false"},
- // "replyable": {"false"},
- // "federated": {"false"},
- // }
- // suite.statusModule.statusGETHandler(ctx)
-
- // // check response
-
- // // 1. we should have OK from our call to the function
- // suite.EqualValues(http.StatusOK, recorder.Code)
-
- // result := recorder.Result()
- // defer result.Body.Close()
- // b, err := ioutil.ReadAll(result.Body)
- // assert.NoError(suite.T(), err)
-
- // statusReply := &mastomodel.Status{}
- // err = json.Unmarshal(b, statusReply)
- // assert.NoError(suite.T(), err)
-
- // assert.Equal(suite.T(), "hello hello", statusReply.SpoilerText)
- // assert.Equal(suite.T(), "this is a brand new status! #helloworld", statusReply.Content)
- // assert.True(suite.T(), statusReply.Sensitive)
- // assert.Equal(suite.T(), mastomodel.VisibilityPrivate, statusReply.Visibility)
- // assert.Len(suite.T(), statusReply.Tags, 1)
- // assert.Equal(suite.T(), mastomodel.Tag{
- // Name: "helloworld",
- // URL: "http://localhost:8080/tags/helloworld",
- // }, statusReply.Tags[0])
-
- // gtsTag := &gtsmodel.Tag{}
- // err = suite.db.GetWhere("name", "helloworld", gtsTag)
- // assert.NoError(suite.T(), err)
- // assert.Equal(suite.T(), statusReply.Account.ID, gtsTag.FirstSeenFromAccountID)
-}
-
-func TestStatusGetTestSuite(t *testing.T) {
- suite.Run(t, new(StatusGetTestSuite))
-}
diff --git a/internal/apimodule/status/test/statusunfave_test.go b/internal/apimodule/status/test/statusunfave_test.go
deleted file mode 100644
index 5f5277921..000000000
--- a/internal/apimodule/status/test/statusunfave_test.go
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package status
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/apimodule/status"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/distributor"
- "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
- mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/testrig"
-)
-
-type StatusUnfaveTestSuite struct {
- // standard suite interfaces
- suite.Suite
- config *config.Config
- db db.DB
- log *logrus.Logger
- storage storage.Storage
- mastoConverter mastotypes.Converter
- mediaHandler media.Handler
- oauthServer oauth.Server
- distributor distributor.Distributor
-
- // standard suite models
- testTokens map[string]*oauth.Token
- testClients map[string]*oauth.Client
- testApplications map[string]*gtsmodel.Application
- testUsers map[string]*gtsmodel.User
- testAccounts map[string]*gtsmodel.Account
- testAttachments map[string]*gtsmodel.MediaAttachment
- testStatuses map[string]*gtsmodel.Status
-
- // module being tested
- statusModule *status.Module
-}
-
-/*
- TEST INFRASTRUCTURE
-*/
-
-// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
-func (suite *StatusUnfaveTestSuite) SetupSuite() {
- // setup standard items
- suite.config = testrig.NewTestConfig()
- suite.db = testrig.NewTestDB()
- suite.log = testrig.NewTestLog()
- suite.storage = testrig.NewTestStorage()
- suite.mastoConverter = testrig.NewTestMastoConverter(suite.db)
- suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
- suite.distributor = testrig.NewTestDistributor()
-
- // setup module being tested
- suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module)
-}
-
-func (suite *StatusUnfaveTestSuite) TearDownSuite() {
- testrig.StandardDBTeardown(suite.db)
- testrig.StandardStorageTeardown(suite.storage)
-}
-
-func (suite *StatusUnfaveTestSuite) SetupTest() {
- testrig.StandardDBSetup(suite.db)
- testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
- suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
- suite.testApplications = testrig.NewTestApplications()
- suite.testUsers = testrig.NewTestUsers()
- suite.testAccounts = testrig.NewTestAccounts()
- suite.testAttachments = testrig.NewTestAttachments()
- suite.testStatuses = testrig.NewTestStatuses()
-}
-
-// TearDownTest drops tables to make sure there's no data in the db
-func (suite *StatusUnfaveTestSuite) TearDownTest() {
- testrig.StandardDBTeardown(suite.db)
- testrig.StandardStorageTeardown(suite.storage)
-}
-
-/*
- ACTUAL TESTS
-*/
-
-// unfave a status
-func (suite *StatusUnfaveTestSuite) TestPostUnfave() {
-
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.TokenToOauthToken(t)
-
- // this is the status we wanna unfave: in the testrig it's already faved by this account
- targetStatus := suite.testStatuses["admin_account_status_1"]
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.UnfavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
-
- // normally the router would populate these params from the path values,
- // but because we're calling the function directly, we need to set them manually.
- ctx.Params = gin.Params{
- gin.Param{
- Key: status.IDKey,
- Value: targetStatus.ID,
- },
- }
-
- suite.statusModule.StatusUnfavePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
-
- statusReply := &mastomodel.Status{}
- err = json.Unmarshal(b, statusReply)
- assert.NoError(suite.T(), err)
-
- assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText)
- assert.Equal(suite.T(), targetStatus.Content, statusReply.Content)
- assert.False(suite.T(), statusReply.Sensitive)
- assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility)
- assert.False(suite.T(), statusReply.Favourited)
- assert.Equal(suite.T(), 0, statusReply.FavouritesCount)
-}
-
-// try to unfave a status that's already not faved
-func (suite *StatusUnfaveTestSuite) TestPostAlreadyNotFaved() {
-
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.TokenToOauthToken(t)
-
- // this is the status we wanna unfave: in the testrig it's not faved by this account
- targetStatus := suite.testStatuses["admin_account_status_2"]
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.UnfavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
-
- // normally the router would populate these params from the path values,
- // but because we're calling the function directly, we need to set them manually.
- ctx.Params = gin.Params{
- gin.Param{
- Key: status.IDKey,
- Value: targetStatus.ID,
- },
- }
-
- suite.statusModule.StatusUnfavePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
-
- statusReply := &mastomodel.Status{}
- err = json.Unmarshal(b, statusReply)
- assert.NoError(suite.T(), err)
-
- assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText)
- assert.Equal(suite.T(), targetStatus.Content, statusReply.Content)
- assert.True(suite.T(), statusReply.Sensitive)
- assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility)
- assert.False(suite.T(), statusReply.Favourited)
- assert.Equal(suite.T(), 0, statusReply.FavouritesCount)
-}
-
-func TestStatusUnfaveTestSuite(t *testing.T) {
- suite.Run(t, new(StatusUnfaveTestSuite))
-}