summaryrefslogtreecommitdiff
path: root/internal/message
diff options
context:
space:
mode:
Diffstat (limited to 'internal/message')
-rw-r--r--internal/message/accountprocess.go168
-rw-r--r--internal/message/adminprocess.go48
-rw-r--r--internal/message/appprocess.go59
-rw-r--r--internal/message/error.go106
-rw-r--r--internal/message/fediprocess.go102
-rw-r--r--internal/message/mediaprocess.go188
-rw-r--r--internal/message/processor.go215
-rw-r--r--internal/message/processorutil.go304
-rw-r--r--internal/message/statusprocess.go350
9 files changed, 1540 insertions, 0 deletions
diff --git a/internal/message/accountprocess.go b/internal/message/accountprocess.go
new file mode 100644
index 000000000..9433140d7
--- /dev/null
+++ b/internal/message/accountprocess.go
@@ -0,0 +1,168 @@
+package message
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+// 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 (p *processor) AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) {
+ l := p.log.WithField("func", "accountCreate")
+
+ if err := p.db.IsEmailAvailable(form.Email); err != nil {
+ return nil, err
+ }
+
+ if err := p.db.IsUsernameAvailable(form.Username); err != nil {
+ return nil, err
+ }
+
+ // don't store a reason if we don't require one
+ reason := form.Reason
+ if !p.config.AccountsConfig.ReasonRequired {
+ reason = ""
+ }
+
+ l.Trace("creating new username and account")
+ user, err := p.db.NewSignup(form.Username, reason, p.config.AccountsConfig.RequireApproval, form.Email, form.Password, form.IP, form.Locale, authed.Application.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, authed.Application.ID)
+ accessToken, err := p.oauthServer.GenerateUserAccessToken(authed.Token, authed.Application.ClientSecret, user.ID)
+ if err != nil {
+ return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)
+ }
+
+ return &apimodel.Token{
+ AccessToken: accessToken.GetAccess(),
+ TokenType: "Bearer",
+ Scope: accessToken.GetScope(),
+ CreatedAt: accessToken.GetAccessCreateAt().Unix(),
+ }, nil
+}
+
+func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) {
+ targetAccount := &gtsmodel.Account{}
+ if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return nil, errors.New("account not found")
+ }
+ return nil, fmt.Errorf("db error: %s", err)
+ }
+
+ var mastoAccount *apimodel.Account
+ var err error
+ if authed.Account != nil && targetAccount.ID == authed.Account.ID {
+ mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
+ } else {
+ mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("error converting account: %s", err)
+ }
+ return mastoAccount, nil
+}
+
+func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
+ l := p.log.WithField("func", "AccountUpdate")
+
+ if form.Discoverable != nil {
+ if err := p.db.UpdateOneByID(authed.Account.ID, "discoverable", *form.Discoverable, &gtsmodel.Account{}); err != nil {
+ return nil, fmt.Errorf("error updating discoverable: %s", err)
+ }
+ }
+
+ if form.Bot != nil {
+ if err := p.db.UpdateOneByID(authed.Account.ID, "bot", *form.Bot, &gtsmodel.Account{}); err != nil {
+ return nil, fmt.Errorf("error updating bot: %s", err)
+ }
+ }
+
+ if form.DisplayName != nil {
+ if err := util.ValidateDisplayName(*form.DisplayName); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(authed.Account.ID, "display_name", *form.DisplayName, &gtsmodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Note != nil {
+ if err := util.ValidateNote(*form.Note); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(authed.Account.ID, "note", *form.Note, &gtsmodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Avatar != nil && form.Avatar.Size != 0 {
+ avatarInfo, err := p.updateAccountAvatar(form.Avatar, authed.Account.ID)
+ if err != nil {
+ return nil, err
+ }
+ l.Tracef("new avatar info for account %s is %+v", authed.Account.ID, avatarInfo)
+ }
+
+ if form.Header != nil && form.Header.Size != 0 {
+ headerInfo, err := p.updateAccountHeader(form.Header, authed.Account.ID)
+ if err != nil {
+ return nil, err
+ }
+ l.Tracef("new header info for account %s is %+v", authed.Account.ID, headerInfo)
+ }
+
+ if form.Locked != nil {
+ if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, &gtsmodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Source != nil {
+ if form.Source.Language != nil {
+ if err := util.ValidateLanguage(*form.Source.Language); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, &gtsmodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Source.Sensitive != nil {
+ if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, &gtsmodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Source.Privacy != nil {
+ if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, &gtsmodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ // fetch the account with all updated values set
+ updatedAccount := &gtsmodel.Account{}
+ if err := p.db.GetByID(authed.Account.ID, updatedAccount); err != nil {
+ return nil, fmt.Errorf("could not fetch updated account %s: %s", authed.Account.ID, err)
+ }
+
+ acctSensitive, err := p.tc.AccountToMastoSensitive(updatedAccount)
+ if err != nil {
+ return nil, fmt.Errorf("could not convert account into mastosensitive account: %s", err)
+ }
+ return acctSensitive, nil
+}
diff --git a/internal/message/adminprocess.go b/internal/message/adminprocess.go
new file mode 100644
index 000000000..abf7b61c7
--- /dev/null
+++ b/internal/message/adminprocess.go
@@ -0,0 +1,48 @@
+package message
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) {
+ if !authed.User.Admin {
+ return nil, fmt.Errorf("user %s not an admin", authed.User.ID)
+ }
+
+ // open the emoji and extract the bytes from it
+ f, err := form.Image.Open()
+ if err != nil {
+ return nil, fmt.Errorf("error opening emoji: %s", err)
+ }
+ buf := new(bytes.Buffer)
+ size, err := io.Copy(buf, f)
+ if err != nil {
+ return nil, fmt.Errorf("error reading emoji: %s", err)
+ }
+ if size == 0 {
+ return nil, errors.New("could not read provided emoji: size 0 bytes")
+ }
+
+ // allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using
+ emoji, err := p.mediaHandler.ProcessLocalEmoji(buf.Bytes(), form.Shortcode)
+ if err != nil {
+ return nil, fmt.Errorf("error reading emoji: %s", err)
+ }
+
+ mastoEmoji, err := p.tc.EmojiToMasto(emoji)
+ if err != nil {
+ return nil, fmt.Errorf("error converting emoji to mastotype: %s", err)
+ }
+
+ if err := p.db.Put(emoji); err != nil {
+ return nil, fmt.Errorf("database error while processing emoji: %s", err)
+ }
+
+ return &mastoEmoji, nil
+}
diff --git a/internal/message/appprocess.go b/internal/message/appprocess.go
new file mode 100644
index 000000000..bf56f0874
--- /dev/null
+++ b/internal/message/appprocess.go
@@ -0,0 +1,59 @@
+package message
+
+import (
+ "github.com/google/uuid"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+func (p *processor) AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) {
+ // 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 := p.db.Put(app); err != nil {
+ return nil, err
+ }
+
+ // 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 := p.db.Put(oc); err != nil {
+ return nil, err
+ }
+
+ mastoApp, err := p.tc.AppToMastoSensitive(app)
+ if err != nil {
+ return nil, err
+ }
+
+ return mastoApp, nil
+}
diff --git a/internal/message/error.go b/internal/message/error.go
new file mode 100644
index 000000000..cbd55dc78
--- /dev/null
+++ b/internal/message/error.go
@@ -0,0 +1,106 @@
+package message
+
+import (
+ "errors"
+ "net/http"
+ "strings"
+)
+
+// ErrorWithCode wraps an internal error with an http code, and a 'safe' version of
+// the error that can be served to clients without revealing internal business logic.
+//
+// A typical use of this error would be to first log the Original error, then return
+// the Safe error and the StatusCode to an API caller.
+type ErrorWithCode interface {
+ // Error returns the original internal error for debugging within the GoToSocial logs.
+ // This should *NEVER* be returned to a client as it may contain sensitive information.
+ Error() string
+ // Safe returns the API-safe version of the error for serialization towards a client.
+ // There's not much point logging this internally because it won't contain much helpful information.
+ Safe() string
+ // Code returns the status code for serving to a client.
+ Code() int
+}
+
+type errorWithCode struct {
+ original error
+ safe error
+ code int
+}
+
+func (e errorWithCode) Error() string {
+ return e.original.Error()
+}
+
+func (e errorWithCode) Safe() string {
+ return e.safe.Error()
+}
+
+func (e errorWithCode) Code() int {
+ return e.code
+}
+
+// NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text.
+func NewErrorBadRequest(original error, helpText ...string) ErrorWithCode {
+ safe := "bad request"
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return errorWithCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusBadRequest,
+ }
+}
+
+// NewErrorNotAuthorized returns an ErrorWithCode 401 with the given original error and optional help text.
+func NewErrorNotAuthorized(original error, helpText ...string) ErrorWithCode {
+ safe := "not authorized"
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return errorWithCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusUnauthorized,
+ }
+}
+
+// NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text.
+func NewErrorForbidden(original error, helpText ...string) ErrorWithCode {
+ safe := "forbidden"
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return errorWithCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusForbidden,
+ }
+}
+
+// NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text.
+func NewErrorNotFound(original error, helpText ...string) ErrorWithCode {
+ safe := "404 not found"
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return errorWithCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusNotFound,
+ }
+}
+
+// NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text.
+func NewErrorInternalError(original error, helpText ...string) ErrorWithCode {
+ safe := "internal server error"
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return errorWithCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusInternalServerError,
+ }
+}
diff --git a/internal/message/fediprocess.go b/internal/message/fediprocess.go
new file mode 100644
index 000000000..6dc6330cf
--- /dev/null
+++ b/internal/message/fediprocess.go
@@ -0,0 +1,102 @@
+package message
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/go-fed/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// authenticateAndDereferenceFediRequest authenticates the HTTP signature of an incoming federation request, using the given
+// username to perform the validation. It will *also* dereference the originator of the request and return it as a gtsmodel account
+// for further processing. NOTE that this function will have the side effect of putting the dereferenced account into the database,
+// and passing it into the processor through a channel for further asynchronous processing.
+func (p *processor) authenticateAndDereferenceFediRequest(username string, r *http.Request) (*gtsmodel.Account, error) {
+
+ // first authenticate
+ requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(username, r)
+ if err != nil {
+ return nil, fmt.Errorf("couldn't authenticate request for username %s: %s", username, err)
+ }
+
+ // OK now we can do the dereferencing part
+ // we might already have an entry for this account so check that first
+ requestingAccount := &gtsmodel.Account{}
+
+ err = p.db.GetWhere("uri", requestingAccountURI.String(), requestingAccount)
+ if err == nil {
+ // we do have it yay, return it
+ return requestingAccount, nil
+ }
+
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ // something has actually gone wrong so bail
+ return nil, fmt.Errorf("database error getting account with uri %s: %s", requestingAccountURI.String(), err)
+ }
+
+ // we just don't have an entry for this account yet
+ // what we do now should depend on our chosen federation method
+ // for now though, we'll just dereference it
+ // TODO: slow-fed
+ requestingPerson, err := p.federator.DereferenceRemoteAccount(username, requestingAccountURI)
+ if err != nil {
+ return nil, fmt.Errorf("couldn't dereference %s: %s", requestingAccountURI.String(), err)
+ }
+
+ // convert it to our internal account representation
+ requestingAccount, err = p.tc.ASRepresentationToAccount(requestingPerson)
+ if err != nil {
+ return nil, fmt.Errorf("couldn't convert dereferenced uri %s to gtsmodel account: %s", requestingAccountURI.String(), err)
+ }
+
+ // shove it in the database for later
+ if err := p.db.Put(requestingAccount); err != nil {
+ return nil, fmt.Errorf("database error inserting account with uri %s: %s", requestingAccountURI.String(), err)
+ }
+
+ // put it in our channel to queue it for async processing
+ p.FromFederator() <- FromFederator{
+ APObjectType: gtsmodel.ActivityStreamsProfile,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ Activity: requestingAccount,
+ }
+
+ return requestingAccount, nil
+}
+
+func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
+ // get the account the request is referring to
+ requestedAccount := &gtsmodel.Account{}
+ if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ }
+
+ // authenticate the request
+ requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
+ if err != nil {
+ return nil, NewErrorNotAuthorized(err)
+ }
+
+ blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ }
+
+ requestedPerson, err := p.tc.AccountToAS(requestedAccount)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ data, err := streams.Serialize(requestedPerson)
+ if err != nil {
+ return nil, NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
diff --git a/internal/message/mediaprocess.go b/internal/message/mediaprocess.go
new file mode 100644
index 000000000..77b387df3
--- /dev/null
+++ b/internal/message/mediaprocess.go
@@ -0,0 +1,188 @@
+package message
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
+ // 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() {
+ return nil, errors.New("not authorized to post new media")
+ }
+
+ // open the attachment and extract the bytes from it
+ f, err := form.File.Open()
+ if err != nil {
+ return nil, fmt.Errorf("error opening attachment: %s", err)
+ }
+ buf := new(bytes.Buffer)
+ size, err := io.Copy(buf, f)
+ if err != nil {
+ return nil, fmt.Errorf("error reading attachment: %s", err)
+
+ }
+ if size == 0 {
+ return nil, errors.New("could not read provided attachment: size 0 bytes")
+ }
+
+ // allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using
+ attachment, err := p.mediaHandler.ProcessLocalAttachment(buf.Bytes(), authed.Account.ID)
+ if err != nil {
+ return nil, fmt.Errorf("error reading attachment: %s", err)
+ }
+
+ // 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 {
+ return nil, fmt.Errorf("improperly formatted focus %s", form.Focus)
+ }
+ xStr := spl[0]
+ yStr := spl[1]
+ if xStr == "" || yStr == "" {
+ return nil, fmt.Errorf("improperly formatted focus %s", form.Focus)
+ }
+ fx, err := strconv.ParseFloat(xStr, 32)
+ if err != nil {
+ return nil, fmt.Errorf("improperly formatted focus %s: %s", form.Focus, err)
+ }
+ if fx > 1 || fx < -1 {
+ return nil, fmt.Errorf("improperly formatted focus %s", form.Focus)
+ }
+ focusx = float32(fx)
+ fy, err := strconv.ParseFloat(yStr, 32)
+ if err != nil {
+ return nil, fmt.Errorf("improperly formatted focus %s: %s", form.Focus, err)
+ }
+ if fy > 1 || fy < -1 {
+ return nil, fmt.Errorf("improperly formatted focus %s", form.Focus)
+ }
+ 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 := p.tc.AttachmentToMasto(attachment)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err)
+ }
+
+ // now we can confidently put the attachment in the database
+ if err := p.db.Put(attachment); err != nil {
+ return nil, fmt.Errorf("error storing media attachment in db: %s", err)
+ }
+
+ return &mastoAttachment, nil
+}
+
+func (p *processor) MediaGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) {
+ // parse the form fields
+ mediaSize, err := media.ParseMediaSize(form.MediaSize)
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize))
+ }
+
+ mediaType, err := media.ParseMediaType(form.MediaType)
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType))
+ }
+
+ spl := strings.Split(form.FileName, ".")
+ if len(spl) != 2 || spl[0] == "" || spl[1] == "" {
+ return nil, NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName))
+ }
+ wantedMediaID := spl[0]
+
+ // get the account that owns the media and make sure it's not suspended
+ acct := &gtsmodel.Account{}
+ if err := p.db.GetByID(form.AccountID, acct); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err))
+ }
+ if !acct.SuspendedAt.IsZero() {
+ return nil, NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID))
+ }
+
+ // make sure the requesting account and the media account don't block each other
+ if authed.Account != nil {
+ blocked, err := p.db.Blocked(authed.Account.ID, form.AccountID)
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, authed.Account.ID, err))
+ }
+ if blocked {
+ return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, authed.Account.ID))
+ }
+ }
+
+ // the way we store emojis is a little different from the way we store other attachments,
+ // so we need to take different steps depending on the media type being requested
+ content := &apimodel.Content{}
+ var storagePath string
+ switch mediaType {
+ case media.Emoji:
+ e := &gtsmodel.Emoji{}
+ if err := p.db.GetByID(wantedMediaID, e); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err))
+ }
+ if e.Disabled {
+ return nil, NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID))
+ }
+ switch mediaSize {
+ case media.Original:
+ content.ContentType = e.ImageContentType
+ storagePath = e.ImagePath
+ case media.Static:
+ content.ContentType = e.ImageStaticContentType
+ storagePath = e.ImageStaticPath
+ default:
+ return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize))
+ }
+ case media.Attachment, media.Header, media.Avatar:
+ a := &gtsmodel.MediaAttachment{}
+ if err := p.db.GetByID(wantedMediaID, a); err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err))
+ }
+ if a.AccountID != form.AccountID {
+ return nil, NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID))
+ }
+ switch mediaSize {
+ case media.Original:
+ content.ContentType = a.File.ContentType
+ storagePath = a.File.Path
+ case media.Small:
+ content.ContentType = a.Thumbnail.ContentType
+ storagePath = a.Thumbnail.Path
+ default:
+ return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
+ }
+ }
+
+ bytes, err := p.storage.RetrieveFileFrom(storagePath)
+ if err != nil {
+ return nil, NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
+ }
+
+ content.ContentLength = int64(len(bytes))
+ content.Content = bytes
+ return content, nil
+}
diff --git a/internal/message/processor.go b/internal/message/processor.go
new file mode 100644
index 000000000..d0027c915
--- /dev/null
+++ b/internal/message/processor.go
@@ -0,0 +1,215 @@
+/*
+ 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 message
+
+import (
+ "net/http"
+
+ "github.com/sirupsen/logrus"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/federation"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/storage"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+)
+
+// Processor should be passed to api modules (see internal/apimodule/...). It is used for
+// passing messages back and forth from the client API and the federating interface, via channels.
+// It also contains logic for filtering which messages should end up where.
+// It is designed to be used asynchronously: the client API and the federating API should just be able to
+// fire messages into the processor and not wait for a reply before proceeding with other work. This allows
+// for clean distribution of messages without slowing down the client API and harming the user experience.
+type Processor interface {
+ // ToClientAPI returns a channel for putting in messages that need to go to the gts client API.
+ ToClientAPI() chan ToClientAPI
+ // FromClientAPI returns a channel for putting messages in that come from the client api going to the processor
+ FromClientAPI() chan FromClientAPI
+ // ToFederator returns a channel for putting in messages that need to go to the federator (activitypub).
+ ToFederator() chan ToFederator
+ // FromFederator returns a channel for putting messages in that come from the federator (activitypub) going into the processor
+ FromFederator() chan FromFederator
+ // Start starts the Processor, reading from its channels and passing messages back and forth.
+ Start() error
+ // Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
+ Stop() error
+
+ /*
+ CLIENT API-FACING PROCESSING FUNCTIONS
+ These functions are intended to be called when the API client needs an immediate (ie., synchronous) reply
+ to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly
+ formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate
+ response, pass work to the processor using a channel instead.
+ */
+
+ // AccountCreate processes the given form for creating a new account, returning an oauth token for that account if successful.
+ AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error)
+ // AccountGet processes the given request for account information.
+ AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error)
+ // AccountUpdate processes the update of an account with the given form
+ AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
+
+ // AppCreate processes the creation of a new API application
+ AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error)
+
+ // StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK.
+ StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error)
+ // StatusDelete processes the delete of a given status, returning the deleted status if the delete goes through.
+ StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+ // StatusFave processes the faving of a given status, returning the updated status if the fave goes through.
+ StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+ // StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
+ StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error)
+ // StatusGet gets the given status, taking account of privacy settings and blocks etc.
+ StatusGet(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+ // StatusUnfave processes the unfaving of a given status, returning the updated status if the fave goes through.
+ StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+
+ // MediaCreate handles the creation of a media attachment, using the given form.
+ MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
+ // MediaGet handles the fetching of a media attachment, using the given request form.
+ MediaGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error)
+ // AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
+ AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
+
+ /*
+ FEDERATION API-FACING PROCESSING FUNCTIONS
+ These functions are intended to be called when the federating client needs an immediate (ie., synchronous) reply
+ to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly
+ formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate
+ response, pass work to the processor using a channel instead.
+ */
+
+ // GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
+ // before returning a JSON serializable interface to the caller.
+ GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
+}
+
+// processor just implements the Processor interface
+type processor struct {
+ // federator pub.FederatingActor
+ toClientAPI chan ToClientAPI
+ fromClientAPI chan FromClientAPI
+ toFederator chan ToFederator
+ fromFederator chan FromFederator
+ federator federation.Federator
+ stop chan interface{}
+ log *logrus.Logger
+ config *config.Config
+ tc typeutils.TypeConverter
+ oauthServer oauth.Server
+ mediaHandler media.Handler
+ storage storage.Storage
+ db db.DB
+}
+
+// NewProcessor returns a new Processor that uses the given federator and logger
+func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator federation.Federator, oauthServer oauth.Server, mediaHandler media.Handler, storage storage.Storage, db db.DB, log *logrus.Logger) Processor {
+ return &processor{
+ toClientAPI: make(chan ToClientAPI, 100),
+ fromClientAPI: make(chan FromClientAPI, 100),
+ toFederator: make(chan ToFederator, 100),
+ fromFederator: make(chan FromFederator, 100),
+ federator: federator,
+ stop: make(chan interface{}),
+ log: log,
+ config: config,
+ tc: tc,
+ oauthServer: oauthServer,
+ mediaHandler: mediaHandler,
+ storage: storage,
+ db: db,
+ }
+}
+
+func (p *processor) ToClientAPI() chan ToClientAPI {
+ return p.toClientAPI
+}
+
+func (p *processor) FromClientAPI() chan FromClientAPI {
+ return p.fromClientAPI
+}
+
+func (p *processor) ToFederator() chan ToFederator {
+ return p.toFederator
+}
+
+func (p *processor) FromFederator() chan FromFederator {
+ return p.fromFederator
+}
+
+// Start starts the Processor, reading from its channels and passing messages back and forth.
+func (p *processor) Start() error {
+ go func() {
+ DistLoop:
+ for {
+ select {
+ case clientMsg := <-p.toClientAPI:
+ p.log.Infof("received message TO client API: %+v", clientMsg)
+ case clientMsg := <-p.fromClientAPI:
+ p.log.Infof("received message FROM client API: %+v", clientMsg)
+ case federatorMsg := <-p.toFederator:
+ p.log.Infof("received message TO federator: %+v", federatorMsg)
+ case federatorMsg := <-p.fromFederator:
+ p.log.Infof("received message FROM federator: %+v", federatorMsg)
+ case <-p.stop:
+ break DistLoop
+ }
+ }
+ }()
+ return nil
+}
+
+// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
+// TODO: empty message buffer properly before stopping otherwise we'll lose federating messages.
+func (p *processor) Stop() error {
+ close(p.stop)
+ return nil
+}
+
+// ToClientAPI wraps a message that travels from the processor into the client API
+type ToClientAPI struct {
+ APObjectType gtsmodel.ActivityStreamsObject
+ APActivityType gtsmodel.ActivityStreamsActivity
+ Activity interface{}
+}
+
+// FromClientAPI wraps a message that travels from client API into the processor
+type FromClientAPI struct {
+ APObjectType gtsmodel.ActivityStreamsObject
+ APActivityType gtsmodel.ActivityStreamsActivity
+ Activity interface{}
+}
+
+// ToFederator wraps a message that travels from the processor into the federator
+type ToFederator struct {
+ APObjectType gtsmodel.ActivityStreamsObject
+ APActivityType gtsmodel.ActivityStreamsActivity
+ Activity interface{}
+}
+
+// FromFederator wraps a message that travels from the federator into the processor
+type FromFederator struct {
+ APObjectType gtsmodel.ActivityStreamsObject
+ APActivityType gtsmodel.ActivityStreamsActivity
+ Activity interface{}
+}
diff --git a/internal/message/processorutil.go b/internal/message/processorutil.go
new file mode 100644
index 000000000..c928eec1a
--- /dev/null
+++ b/internal/message/processorutil.go
@@ -0,0 +1,304 @@
+package message
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "mime/multipart"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) processVisibility(form *apimodel.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 = gtsmodel.Visibility(*form.VisibilityAdvanced)
+ } else if form.Visibility != "" {
+ gtsBasicVis = p.tc.MastoVisToVis(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 (p *processor) processReplyToID(form *apimodel.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 := p.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 := p.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 := p.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 (p *processor) processMediaIDs(form *apimodel.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 := p.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 (p *processor) processLanguage(form *apimodel.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 (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ menchies := []string{}
+ gtsMenchies, err := p.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 := p.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 (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ tags := []string{}
+ gtsTags, err := p.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 := p.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 (p *processor) processEmojis(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ emojis := []string{}
+ gtsEmojis, err := p.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
+}
+
+/*
+ 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 (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
+ var err error
+ if int(avatar.Size) > p.config.MediaConfig.MaxImageSize {
+ err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, p.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 := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar)
+ 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 (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
+ var err error
+ if int(header.Size) > p.config.MediaConfig.MaxImageSize {
+ err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, p.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 := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header)
+ if err != nil {
+ return nil, fmt.Errorf("error processing header: %s", err)
+ }
+
+ return headerInfo, f.Close()
+}
diff --git a/internal/message/statusprocess.go b/internal/message/statusprocess.go
new file mode 100644
index 000000000..b7237fecf
--- /dev/null
+++ b/internal/message/statusprocess.go
@@ -0,0 +1,350 @@
+package message
+
+import (
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) StatusCreate(auth *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) {
+ uris := util.GenerateURIsForAccount(auth.Account.Username, p.config.Protocol, p.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: auth.Account.ID,
+ ContentWarning: form.SpoilerText,
+ ActivityStreamsType: gtsmodel.ActivityStreamsNote,
+ Sensitive: form.Sensitive,
+ Language: form.Language,
+ CreatedWithApplicationID: auth.Application.ID,
+ Text: form.Status,
+ }
+
+ // check if replyToID is ok
+ if err := p.processReplyToID(form, auth.Account.ID, newStatus); err != nil {
+ return nil, err
+ }
+
+ // check if mediaIDs are ok
+ if err := p.processMediaIDs(form, auth.Account.ID, newStatus); err != nil {
+ return nil, err
+ }
+
+ // check if visibility settings are ok
+ if err := p.processVisibility(form, auth.Account.Privacy, newStatus); err != nil {
+ return nil, err
+ }
+
+ // handle language settings
+ if err := p.processLanguage(form, auth.Account.Language, newStatus); err != nil {
+ return nil, err
+ }
+
+ // handle mentions
+ if err := p.processMentions(form, auth.Account.ID, newStatus); err != nil {
+ return nil, err
+ }
+
+ if err := p.processTags(form, auth.Account.ID, newStatus); err != nil {
+ return nil, err
+ }
+
+ if err := p.processEmojis(form, auth.Account.ID, newStatus); err != nil {
+ return nil, err
+ }
+
+ // put the new status in the database, generating an ID for it in the process
+ if err := p.db.Put(newStatus); err != nil {
+ return nil, err
+ }
+
+ // 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 := p.db.UpdateByID(a.ID, a); err != nil {
+ return nil, err
+ }
+ }
+
+ // return the frontend representation of the new status to the submitter
+ return p.tc.StatusToMasto(newStatus, auth.Account, auth.Account, nil, newStatus.GTSReplyToAccount, nil)
+}
+
+func (p *processor) StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+ l := p.log.WithField("func", "StatusDelete")
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
+ }
+
+ if targetStatus.AccountID != authed.Account.ID {
+ return nil, errors.New("status doesn't belong to requesting account")
+ }
+
+ l.Trace("going to get relevant accounts")
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, authed.Account, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
+ }
+
+ if err := p.db.DeleteByID(targetStatus.ID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error deleting status from the database: %s", err)
+ }
+
+ return mastoStatus, nil
+}
+
+func (p *processor) StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+ l := p.log.WithField("func", "StatusFave")
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := &gtsmodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
+ }
+
+ l.Trace("going to get relevant accounts")
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.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 {
+ return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
+ }
+
+ if !visible {
+ return nil, errors.New("status is not visible")
+ }
+
+ // is the status faveable?
+ if !targetStatus.VisibilityAdvanced.Likeable {
+ return nil, errors.New("status is not faveable")
+ }
+
+ // it's visible! it's faveable! so let's fave the FUCK out of it
+ _, err = p.db.FaveStatus(targetStatus, authed.Account.ID)
+ if err != nil {
+ return nil, fmt.Errorf("error faveing status: %s", err)
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
+ }
+
+ return mastoStatus, nil
+}
+
+func (p *processor) StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) {
+ l := p.log.WithField("func", "StatusFavedBy")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := &gtsmodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
+ }
+
+ l.Trace("going to get relevant accounts")
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.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 {
+ return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
+ }
+
+ if !visible {
+ return nil, errors.New("status is not visible")
+ }
+
+ // get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
+ favingAccounts, err := p.db.WhoFavedStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error seeing who faved status: %s", err)
+ }
+
+ // 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 := p.db.Blocked(authed.Account.ID, acc.ID)
+ if err != nil {
+ return nil, fmt.Errorf("error checking blocks: %s", err)
+ }
+ 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 := []*apimodel.Account{}
+ for _, acc := range filteredAccounts {
+ mastoAccount, err := p.tc.AccountToMastoPublic(acc)
+ if err != nil {
+ return nil, fmt.Errorf("error converting account to api model: %s", err)
+ }
+ mastoAccounts = append(mastoAccounts, mastoAccount)
+ }
+
+ return mastoAccounts, nil
+}
+
+func (p *processor) StatusGet(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+ l := p.log.WithField("func", "StatusGet")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := &gtsmodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
+ }
+
+ l.Trace("going to get relevant accounts")
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.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 {
+ return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
+ }
+
+ if !visible {
+ return nil, errors.New("status is not visible")
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
+ }
+
+ return mastoStatus, nil
+
+}
+
+func (p *processor) StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+ l := p.log.WithField("func", "StatusUnfave")
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := &gtsmodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
+ }
+
+ l.Trace("going to get relevant accounts")
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.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 {
+ return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
+ }
+
+ if !visible {
+ return nil, errors.New("status is not visible")
+ }
+
+ // is the status faveable?
+ if !targetStatus.VisibilityAdvanced.Likeable {
+ return nil, errors.New("status is not faveable")
+ }
+
+ // it's visible! it's faveable! so let's unfave the FUCK out of it
+ _, err = p.db.UnfaveStatus(targetStatus, authed.Account.ID)
+ if err != nil {
+ return nil, fmt.Errorf("error unfaveing status: %s", err)
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
+ if err != nil {
+ return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
+ }
+
+ return mastoStatus, nil
+}