summaryrefslogtreecommitdiff
path: root/internal/processing/status
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing/status')
-rw-r--r--internal/processing/status/boost.go73
-rw-r--r--internal/processing/status/boostedby.go68
-rw-r--r--internal/processing/status/context.go69
-rw-r--r--internal/processing/status/create.go106
-rw-r--r--internal/processing/status/delete.go56
-rw-r--r--internal/processing/status/fave.go101
-rw-r--r--internal/processing/status/favedby.go68
-rw-r--r--internal/processing/status/get.go52
-rw-r--r--internal/processing/status/status.go57
-rw-r--r--internal/processing/status/unboost.go95
-rw-r--r--internal/processing/status/unfave.go77
-rw-r--r--internal/processing/status/util.go269
12 files changed, 1091 insertions, 0 deletions
diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go
new file mode 100644
index 000000000..93d0f19de
--- /dev/null
+++ b/internal/processing/status/boost.go
@@ -0,0 +1,73 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ l := p.log.WithField("func", "StatusBoost")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ if targetStatus.VisibilityAdvanced != nil {
+ if !targetStatus.VisibilityAdvanced.Boostable {
+ return nil, gtserror.NewErrorForbidden(errors.New("status is not boostable"))
+ }
+ }
+
+ // it's visible! it's boostable! so let's boost the FUCK out of it
+ boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ boostWrapperStatus.CreatedWithApplicationID = application.ID
+ boostWrapperStatus.GTSBoostedAccount = targetAccount
+
+ // put the boost in the database
+ if err := p.db.Put(boostWrapperStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsAnnounce,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: boostWrapperStatus,
+ OriginAccount: account,
+ TargetAccount: targetAccount,
+ }
+
+ // return the frontend representation of the new status to the submitter
+ mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/boostedby.go b/internal/processing/status/boostedby.go
new file mode 100644
index 000000000..b352178e3
--- /dev/null
+++ b/internal/processing/status/boostedby.go
@@ -0,0 +1,68 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
+ l := p.log.WithField("func", "StatusBoostedBy")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: 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, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("StatusBoostedBy: 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.WhoBoostedStatus(targetStatus)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing who boosted 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(account.ID, acc.ID)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: 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, gtserror.NewErrorNotFound(fmt.Errorf("StatusFavedBy: error converting account to api model: %s", err))
+ }
+ mastoAccounts = append(mastoAccounts, mastoAccount)
+ }
+
+ return mastoAccounts, nil
+}
diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go
new file mode 100644
index 000000000..72b9b5623
--- /dev/null
+++ b/internal/processing/status/context.go
@@ -0,0 +1,69 @@
+package status
+
+import (
+ "fmt"
+ "sort"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
+
+ context := &apimodel.Context{
+ Ancestors: []apimodel.Status{},
+ Descendants: []apimodel.Status{},
+ }
+
+ targetStatus := &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+ if !visible {
+ return nil, gtserror.NewErrorForbidden(fmt.Errorf("account with id %s does not have permission to view status %s", account.ID, targetStatusID))
+ }
+
+ parents, err := p.db.StatusParents(targetStatus)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ for _, status := range parents {
+ if v, err := p.filter.StatusVisible(status, account); err == nil && v {
+ mastoStatus, err := p.tc.StatusToMasto(status, account)
+ if err == nil {
+ context.Ancestors = append(context.Ancestors, *mastoStatus)
+ }
+ }
+ }
+
+ sort.Slice(context.Ancestors, func(i int, j int) bool {
+ return context.Ancestors[i].ID < context.Ancestors[j].ID
+ })
+
+ children, err := p.db.StatusChildren(targetStatus)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ for _, status := range children {
+ if v, err := p.filter.StatusVisible(status, account); err == nil && v {
+ mastoStatus, err := p.tc.StatusToMasto(status, account)
+ if err == nil {
+ context.Descendants = append(context.Descendants, *mastoStatus)
+ }
+ }
+ }
+
+ return context, nil
+}
diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go
new file mode 100644
index 000000000..aa7468ae5
--- /dev/null
+++ b/internal/processing/status/create.go
@@ -0,0 +1,106 @@
+package status
+
+import (
+ "fmt"
+ "time"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
+ uris := util.GenerateURIsForAccount(account.Username, p.config.Protocol, p.config.Host)
+ thisStatusID, err := id.NewULID()
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ 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,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ Local: true,
+ AccountID: account.ID,
+ AccountURI: account.URI,
+ ContentWarning: form.SpoilerText,
+ ActivityStreamsType: gtsmodel.ActivityStreamsNote,
+ Sensitive: form.Sensitive,
+ Language: form.Language,
+ CreatedWithApplicationID: application.ID,
+ Text: form.Status,
+ }
+
+ // check if replyToID is ok
+ if err := p.processReplyToID(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // check if mediaIDs are ok
+ if err := p.processMediaIDs(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // check if visibility settings are ok
+ if err := p.processVisibility(form, account.Privacy, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // handle language settings
+ if err := p.processLanguage(form, account.Language, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // handle mentions
+ if err := p.processMentions(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if err := p.processTags(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if err := p.processEmojis(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if err := p.processContent(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(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, gtserror.NewErrorInternalError(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, gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsNote,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: newStatus,
+ OriginAccount: account,
+ }
+
+ // return the frontend representation of the new status to the submitter
+ mastoStatus, err := p.tc.StatusToMasto(newStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", newStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/delete.go b/internal/processing/status/delete.go
new file mode 100644
index 000000000..259038dee
--- /dev/null
+++ b/internal/processing/status/delete.go
@@ -0,0 +1,56 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ 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 {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+ // status is already gone
+ return nil, nil
+ }
+
+ if targetStatus.AccountID != account.ID {
+ return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account"))
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ if err := p.db.DeleteByID(targetStatus.ID, &gtsmodel.Status{}); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error deleting status from the database: %s", err))
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsNote,
+ APActivityType: gtsmodel.ActivityStreamsDelete,
+ GTSModel: targetStatus,
+ OriginAccount: account,
+ TargetAccount: account,
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go
new file mode 100644
index 000000000..23f0d2944
--- /dev/null
+++ b/internal/processing/status/fave.go
@@ -0,0 +1,101 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ 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, gtserror.NewErrorNotFound(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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
+ }
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ // is the status faveable?
+ if targetStatus.VisibilityAdvanced != nil {
+ if !targetStatus.VisibilityAdvanced.Likeable {
+ return nil, gtserror.NewErrorForbidden(errors.New("status is not faveable"))
+ }
+ }
+
+ // first check if the status is already faved, if so we don't need to do anything
+ newFave := true
+ gtsFave := &gtsmodel.StatusFave{}
+ if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err == nil {
+ // we already have a fave for this status
+ newFave = false
+ }
+
+ if newFave {
+ thisFaveID, err := id.NewRandomULID()
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // we need to create a new fave in the database
+ gtsFave := &gtsmodel.StatusFave{
+ ID: thisFaveID,
+ AccountID: account.ID,
+ TargetAccountID: targetAccount.ID,
+ StatusID: targetStatus.ID,
+ URI: util.GenerateURIForLike(account.Username, p.config.Protocol, p.config.Host, thisFaveID),
+ GTSStatus: targetStatus,
+ GTSTargetAccount: targetAccount,
+ GTSFavingAccount: account,
+ }
+
+ if err := p.db.Put(gtsFave); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting fave in database: %s", err))
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsLike,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: gtsFave,
+ OriginAccount: account,
+ TargetAccount: targetAccount,
+ }
+ }
+
+ // return the mastodon representation of the target status
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/favedby.go b/internal/processing/status/favedby.go
new file mode 100644
index 000000000..5194cc258
--- /dev/null
+++ b/internal/processing/status/favedby.go
@@ -0,0 +1,68 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) FavedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
+ 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, gtserror.NewErrorNotFound(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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(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, gtserror.NewErrorNotFound(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(account.ID, acc.ID)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(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, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+ mastoAccounts = append(mastoAccounts, mastoAccount)
+ }
+
+ return mastoAccounts, nil
+}
diff --git a/internal/processing/status/get.go b/internal/processing/status/get.go
new file mode 100644
index 000000000..9a70185b0
--- /dev/null
+++ b/internal/processing/status/get.go
@@ -0,0 +1,52 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Get(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ 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, gtserror.NewErrorNotFound(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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(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, gtserror.NewErrorInternalError(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+
+}
diff --git a/internal/processing/status/status.go b/internal/processing/status/status.go
new file mode 100644
index 000000000..d83c325fd
--- /dev/null
+++ b/internal/processing/status/status.go
@@ -0,0 +1,57 @@
+package status
+
+import (
+ "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/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
+)
+
+// Processor wraps a bunch of functions for processing statuses.
+type Processor interface {
+ // Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
+ Create(account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode)
+ // Delete processes the delete of a given status, returning the deleted status if the delete goes through.
+ Delete(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // Fave processes the faving of a given status, returning the updated status if the fave goes through.
+ Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // Boost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
+ Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // Unboost processes the unboost/unreblog of a given status, returning the status if all is well.
+ Unboost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // BoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
+ BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
+ // FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
+ FavedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
+ // Get gets the given status, taking account of privacy settings and blocks etc.
+ Get(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // Unfave processes the unfaving of a given status, returning the updated status if the fave goes through.
+ Unfave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // Context returns the context (previous and following posts) from the given status ID
+ Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
+}
+
+type processor struct {
+ tc typeutils.TypeConverter
+ config *config.Config
+ db db.DB
+ filter visibility.Filter
+ fromClientAPI chan gtsmodel.FromClientAPI
+ log *logrus.Logger
+}
+
+// New returns a new status processor.
+func New(db db.DB, tc typeutils.TypeConverter, config *config.Config, fromClientAPI chan gtsmodel.FromClientAPI, log *logrus.Logger) Processor {
+ return &processor{
+ tc: tc,
+ config: config,
+ db: db,
+ filter: visibility.NewFilter(db, log),
+ fromClientAPI: fromClientAPI,
+ log: log,
+ }
+}
diff --git a/internal/processing/status/unboost.go b/internal/processing/status/unboost.go
new file mode 100644
index 000000000..2a1394695
--- /dev/null
+++ b/internal/processing/status/unboost.go
@@ -0,0 +1,95 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Unboost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ l := p.log.WithField("func", "Unboost")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ // check if we actually have a boost for this status
+ var toUnboost bool
+
+ gtsBoost := &gtsmodel.Status{}
+ where := []db.Where{
+ {
+ Key: "boost_of_id",
+ Value: targetStatusID,
+ },
+ {
+ Key: "account_id",
+ Value: account.ID,
+ },
+ }
+ err = p.db.GetWhere(where, gtsBoost)
+ if err == nil {
+ // we have a boost
+ toUnboost = true
+ }
+
+ if err != nil {
+ // something went wrong in the db finding the boost
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err))
+ }
+ // we just don't have a boost
+ toUnboost = false
+ }
+
+ if toUnboost {
+ // we had a boost, so take some action to get rid of it
+ if err := p.db.DeleteWhere(where, &gtsmodel.Status{}); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unboosting status: %s", err))
+ }
+
+ // pin some stuff onto the boost while we have it out of the db
+ gtsBoost.GTSBoostedStatus = targetStatus
+ gtsBoost.GTSBoostedStatus.GTSAuthorAccount = targetAccount
+ gtsBoost.GTSBoostedAccount = targetAccount
+ gtsBoost.GTSAuthorAccount = account
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsAnnounce,
+ APActivityType: gtsmodel.ActivityStreamsUndo,
+ GTSModel: gtsBoost,
+ OriginAccount: account,
+ TargetAccount: targetAccount,
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/unfave.go b/internal/processing/status/unfave.go
new file mode 100644
index 000000000..b51daacb9
--- /dev/null
+++ b/internal/processing/status/unfave.go
@@ -0,0 +1,77 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ 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, gtserror.NewErrorNotFound(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, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ // check if we actually have a fave for this status
+ var toUnfave bool
+
+ gtsFave := &gtsmodel.StatusFave{}
+ err = p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave)
+ if err == nil {
+ // we have a fave
+ toUnfave = true
+ }
+ if err != nil {
+ // something went wrong in the db finding the fave
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err))
+ }
+ // we just don't have a fave
+ toUnfave = false
+ }
+
+ if toUnfave {
+ // we had a fave, so take some action to get rid of it
+ if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsLike,
+ APActivityType: gtsmodel.ActivityStreamsUndo,
+ GTSModel: gtsFave,
+ OriginAccount: account,
+ TargetAccount: targetAccount,
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go
new file mode 100644
index 000000000..0a023eab6
--- /dev/null
+++ b/internal/processing/status/util.go
@@ -0,0 +1,269 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ 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/id"
+ "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 != nil {
+ 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.DeriveMentionsFromStatus(form.Status), accountID, status.ID)
+ if err != nil {
+ return fmt.Errorf("error generating mentions from status: %s", err)
+ }
+ for _, menchie := range gtsMenchies {
+ menchieID, err := id.NewRandomULID()
+ if err != nil {
+ return err
+ }
+ menchie.ID = menchieID
+
+ if err := p.db.Put(menchie); err != nil {
+ return fmt.Errorf("error putting mentions in db: %s", err)
+ }
+ menchies = append(menchies, menchie.ID)
+ }
+ // 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.DeriveHashtagsFromStatus(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.DeriveEmojisFromStatus(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
+}
+
+func (p *processor) processContent(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ if form.Status == "" {
+ status.Content = ""
+ return nil
+ }
+
+ // surround the whole status in '<p>'
+ content := fmt.Sprintf(`<p>%s</p>`, form.Status)
+
+ // format mentions nicely
+ for _, menchie := range status.GTSMentions {
+ targetAccount := &gtsmodel.Account{}
+ if err := p.db.GetByID(menchie.TargetAccountID, targetAccount); err == nil {
+ mentionContent := fmt.Sprintf(`<span class="h-card"><a href="%s" class="u-url mention">@<span>%s</span></a></span>`, targetAccount.URL, targetAccount.Username)
+ content = strings.ReplaceAll(content, menchie.NameString, mentionContent)
+ }
+ }
+
+ // format tags nicely
+ for _, tag := range status.GTSTags {
+ tagContent := fmt.Sprintf(`<a href="%s" class="mention hashtag" rel="tag">#<span>%s</span></a>`, tag.URL, tag.Name)
+ content = strings.ReplaceAll(content, fmt.Sprintf("#%s", tag.Name), tagContent)
+ }
+
+ // replace newlines with breaks
+ content = strings.ReplaceAll(content, "\n", "<br />")
+
+ status.Content = content
+ return nil
+}