[feature] tentatively start adding polls support (#2249)

This commit is contained in:
kim
2023-10-04 13:09:42 +01:00
committed by GitHub
parent 297b6eeaaa
commit c6e00afc7c
36 changed files with 657 additions and 393 deletions

View File

@@ -34,70 +34,75 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
//
// Precondition: the form's fields should have already been validated and normalized by the caller.
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
accountURIs := uris.GenerateURIsForAccount(account.Username)
thisStatusID := id.NewULID()
local := true
sensitive := form.Sensitive
func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
// Generate new ID for status.
statusID := id.NewULID()
newStatus := &gtsmodel.Status{
ID: thisStatusID,
URI: accountURIs.StatusesURI + "/" + thisStatusID,
URL: accountURIs.StatusesURL + "/" + thisStatusID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Local: &local,
AccountID: account.ID,
AccountURI: account.URI,
ContentWarning: text.SanitizeToPlaintext(form.SpoilerText),
// Generate necessary URIs for username, to build status URIs.
accountURIs := uris.GenerateURIsForAccount(requestingAccount.Username)
// Get current time.
now := time.Now()
status := &gtsmodel.Status{
ID: statusID,
URI: accountURIs.StatusesURI + "/" + statusID,
URL: accountURIs.StatusesURL + "/" + statusID,
CreatedAt: now,
UpdatedAt: now,
Local: util.Ptr(true),
Account: requestingAccount,
AccountID: requestingAccount.ID,
AccountURI: requestingAccount.URI,
ActivityStreamsType: ap.ObjectNote,
Sensitive: &sensitive,
Sensitive: &form.Sensitive,
CreatedWithApplicationID: application.ID,
Text: form.Status,
}
if errWithCode := processReplyToID(ctx, p.state.DB, form, account.ID, newStatus); errWithCode != nil {
if errWithCode := p.processReplyToID(ctx, form, requestingAccount.ID, status); errWithCode != nil {
return nil, errWithCode
}
if errWithCode := processMediaIDs(ctx, p.state.DB, form, account.ID, newStatus); errWithCode != nil {
if errWithCode := p.processMediaIDs(ctx, form, requestingAccount.ID, status); errWithCode != nil {
return nil, errWithCode
}
if err := processVisibility(ctx, form, account.Privacy, newStatus); err != nil {
if err := processVisibility(form, requestingAccount.Privacy, status); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if err := processLanguage(ctx, form, account.Language, newStatus); err != nil {
if err := processLanguage(form, requestingAccount.Language, status); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if err := processContent(ctx, p.state.DB, p.formatter, p.parseMention, form, account.ID, newStatus); err != nil {
if err := p.processContent(ctx, p.parseMention, form, status); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
// put the new status in the database
if err := p.state.DB.PutStatus(ctx, newStatus); err != nil {
// Insert this new status in the database.
if err := p.state.DB.PutStatus(ctx, status); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
// send it back to the processor for async processing
// send it back to the client API worker for async side-effects.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: newStatus,
OriginAccount: account,
GTSModel: status,
OriginAccount: requestingAccount,
})
return p.apiStatus(ctx, newStatus, account)
return p.apiStatus(ctx, status, requestingAccount)
}
func processReplyToID(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
func (p *Processor) processReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
if form.InReplyToID == "" {
return nil
}
@@ -109,78 +114,74 @@ func processReplyToID(ctx context.Context, dbService db.DB, form *apimodel.Advan
// 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{}
if err := dbService.GetByID(ctx, form.InReplyToID, repliedStatus); err != nil {
if err == db.ErrNoEntries {
err := fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
err := fmt.Errorf("db error fetching status with id %s: %s", form.InReplyToID, err)
return gtserror.NewErrorInternalError(err)
}
if !*repliedStatus.Replyable {
err := fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
return gtserror.NewErrorForbidden(err, err.Error())
}
if err := dbService.GetByID(ctx, repliedStatus.AccountID, repliedAccount); err != nil {
if err == db.ErrNoEntries {
err := fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
err := fmt.Errorf("db error fetching account with id %s: %s", repliedStatus.AccountID, err)
inReplyTo, err := p.state.DB.GetStatusByID(ctx, form.InReplyToID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("error fetching status %s from db: %w", form.InReplyToID, err)
return gtserror.NewErrorInternalError(err)
}
if blocked, err := dbService.IsEitherBlocked(ctx, thisAccountID, repliedAccount.ID); err != nil {
err := fmt.Errorf("db error checking block: %s", err)
if inReplyTo == nil {
const text = "cannot reply to status that does not exist"
return gtserror.NewErrorBadRequest(errors.New(text), text)
}
if !*inReplyTo.Replyable {
text := fmt.Sprintf("status %s is marked as not replyable", form.InReplyToID)
return gtserror.NewErrorForbidden(errors.New(text), text)
}
if blocked, err := p.state.DB.IsEitherBlocked(ctx, thisAccountID, inReplyTo.AccountID); err != nil {
err := gtserror.Newf("error checking block in db: %w", err)
return gtserror.NewErrorInternalError(err)
} else if blocked {
err := fmt.Errorf("status with id %s not replyable", form.InReplyToID)
return gtserror.NewErrorNotFound(err)
text := fmt.Sprintf("status %s is not replyable", form.InReplyToID)
return gtserror.NewErrorNotFound(errors.New(text), text)
}
status.InReplyToID = repliedStatus.ID
status.InReplyToURI = repliedStatus.URI
status.InReplyToAccountID = repliedAccount.ID
// Set status fields from inReplyTo.
status.InReplyToID = inReplyTo.ID
status.InReplyToURI = inReplyTo.URI
status.InReplyToAccountID = inReplyTo.AccountID
return nil
}
func processMediaIDs(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
func (p *Processor) processMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
if form.MediaIDs == nil {
return nil
}
// Get minimum allowed char descriptions.
minChars := config.GetMediaDescriptionMinChars()
attachments := []*gtsmodel.MediaAttachment{}
attachmentIDs := []string{}
for _, mediaID := range form.MediaIDs {
attachment, err := dbService.GetAttachmentByID(ctx, mediaID)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("ProcessMediaIDs: media not found for media id %s", mediaID)
return gtserror.NewErrorBadRequest(err, err.Error())
}
err = fmt.Errorf("ProcessMediaIDs: db error for media id %s", mediaID)
attachment, err := p.state.DB.GetAttachmentByID(ctx, mediaID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("error fetching media from db: %w", err)
return gtserror.NewErrorInternalError(err)
}
if attachment == nil {
text := fmt.Sprintf("media %s not found", mediaID)
return gtserror.NewErrorBadRequest(errors.New(text), text)
}
if attachment.AccountID != thisAccountID {
err = fmt.Errorf("ProcessMediaIDs: media with id %s does not belong to account %s", mediaID, thisAccountID)
return gtserror.NewErrorBadRequest(err, err.Error())
text := fmt.Sprintf("media %s does not belong to account", mediaID)
return gtserror.NewErrorBadRequest(errors.New(text), text)
}
if attachment.StatusID != "" || attachment.ScheduledStatusID != "" {
err = fmt.Errorf("ProcessMediaIDs: media with id %s is already attached to a status", mediaID)
return gtserror.NewErrorBadRequest(err, err.Error())
text := fmt.Sprintf("media %s already attached to status", mediaID)
return gtserror.NewErrorBadRequest(errors.New(text), text)
}
minDescriptionChars := config.GetMediaDescriptionMinChars()
if descriptionLength := len([]rune(attachment.Description)); descriptionLength < minDescriptionChars {
err = fmt.Errorf("ProcessMediaIDs: description too short! media description of at least %d chararacters is required but %d was provided for media with id %s", minDescriptionChars, descriptionLength, mediaID)
return gtserror.NewErrorBadRequest(err, err.Error())
if length := len([]rune(attachment.Description)); length < minChars {
text := fmt.Sprintf("media %s description too short, at least %d required", mediaID, minChars)
return gtserror.NewErrorBadRequest(errors.New(text), text)
}
attachments = append(attachments, attachment)
@@ -192,7 +193,7 @@ func processMediaIDs(ctx context.Context, dbService db.DB, form *apimodel.Advanc
return nil
}
func processVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
func processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
// by default all flags are set to true
federated := true
boostable := true
@@ -265,7 +266,7 @@ func processVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateF
return nil
}
func processLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
func processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
if form.Language != "" {
status.Language = form.Language
} else {
@@ -277,68 +278,80 @@ func processLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateFor
return nil
}
func processContent(ctx context.Context, dbService db.DB, formatter *text.Formatter, parseMention gtsmodel.ParseMentionFunc, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
// if there's nothing in the status at all we can just return early
if form.Status == "" {
status.Content = ""
return nil
}
// if content type wasn't specified we should try to figure out what content type this user prefers
func (p *Processor) processContent(ctx context.Context, parseMention gtsmodel.ParseMentionFunc, form *apimodel.AdvancedStatusCreateForm, status *gtsmodel.Status) error {
if form.ContentType == "" {
acct, err := dbService.GetAccountByID(ctx, accountID)
if err != nil {
return fmt.Errorf("error processing new content: couldn't retrieve account from db to check post format: %s", err)
}
switch acct.StatusContentType {
case "text/plain":
form.ContentType = apimodel.StatusContentTypePlain
case "text/markdown":
form.ContentType = apimodel.StatusContentTypeMarkdown
default:
form.ContentType = apimodel.StatusContentTypeDefault
}
// If content type wasn't specified, use the author's preferred content-type.
contentType := apimodel.StatusContentType(status.Account.StatusContentType)
form.ContentType = contentType
}
// format is the currently set text formatting
// function, according to the provided content-type.
var format text.FormatFunc
// formatInput is a shorthand function to format the given input string with the
// currently set 'formatFunc', passing in all required args and returning result.
formatInput := func(formatFunc text.FormatFunc, input string) *text.FormatResult {
return formatFunc(ctx, parseMention, status.AccountID, status.ID, input)
}
// parse content out of the status depending on what content type has been submitted
var f text.FormatFunc
switch form.ContentType {
// None given / set,
// use default (plain).
case "":
fallthrough
// Format status according to text/plain.
case apimodel.StatusContentTypePlain:
f = formatter.FromPlain
format = p.formatter.FromPlain
// Format status according to text/markdown.
case apimodel.StatusContentTypeMarkdown:
f = formatter.FromMarkdown
format = p.formatter.FromMarkdown
// Unknown.
default:
return fmt.Errorf("format %s not recognised as a valid status format", form.ContentType)
}
formatted := f(ctx, parseMention, accountID, status.ID, form.Status)
// add full populated gts {mentions, tags, emojis} to the status for passing them around conveniently
// add just their ids to the status for putting in the db
status.Mentions = formatted.Mentions
status.MentionIDs = make([]string, 0, len(formatted.Mentions))
for _, gtsmention := range formatted.Mentions {
status.MentionIDs = append(status.MentionIDs, gtsmention.ID)
return fmt.Errorf("invalid status format: %q", form.ContentType)
}
status.Tags = formatted.Tags
status.TagIDs = make([]string, 0, len(formatted.Tags))
for _, gtstag := range formatted.Tags {
status.TagIDs = append(status.TagIDs, gtstag.ID)
}
// Sanitize status text and format.
contentRes := formatInput(format, form.Status)
status.Emojis = formatted.Emojis
status.EmojiIDs = make([]string, 0, len(formatted.Emojis))
for _, gtsemoji := range formatted.Emojis {
status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
}
// Collect formatted results.
status.Content = contentRes.HTML
status.Mentions = append(status.Mentions, contentRes.Mentions...)
status.Emojis = append(status.Emojis, contentRes.Emojis...)
status.Tags = append(status.Tags, contentRes.Tags...)
spoilerformatted := formatter.FromPlainEmojiOnly(ctx, parseMention, accountID, status.ID, form.SpoilerText)
for _, gtsemoji := range spoilerformatted.Emojis {
status.Emojis = append(status.Emojis, gtsemoji)
status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
}
// From here-on-out just use emoji-only
// plain-text formatting as the FormatFunc.
format = p.formatter.FromPlainEmojiOnly
// Sanitize content warning and format.
spoiler := text.SanitizeToPlaintext(form.SpoilerText)
warningRes := formatInput(format, spoiler)
// Collect formatted results.
status.ContentWarning = warningRes.HTML
status.Emojis = append(status.Emojis, warningRes.Emojis...)
// Gather all the database IDs from each of the gathered status mentions, tags, and emojis.
status.MentionIDs = gatherIDs(status.Mentions, func(mention *gtsmodel.Mention) string { return mention.ID })
status.TagIDs = gatherIDs(status.Tags, func(tag *gtsmodel.Tag) string { return tag.ID })
status.EmojiIDs = gatherIDs(status.Emojis, func(emoji *gtsmodel.Emoji) string { return emoji.ID })
status.Content = formatted.HTML
return nil
}
// gatherIDs is a small utility function to gather IDs from a slice of type T.
func gatherIDs[T any](in []T, getID func(T) string) []string {
if getID == nil {
// move nil check out loop.
panic("nil getID function")
}
ids := make([]string, len(in))
for i, t := range in {
ids[i] = getID(t)
}
return ids
}

View File

@@ -204,7 +204,7 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() {
}
apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm)
suite.EqualError(err, "ProcessMediaIDs: description too short! media description of at least 100 chararacters is required but 15 was provided for media with id 01F8MH8RMYQ6MSNY3JM2XT1CQ5")
suite.EqualError(err, "media 01F8MH8RMYQ6MSNY3JM2XT1CQ5 description too short, at least 100 required")
suite.Nil(apiStatus)
}

View File

@@ -46,7 +46,7 @@ func (p *Processor) toAccount(payload string, event string, streamTypes []string
if !ok {
return nil // No entry = nothing to stream.
}
streamsForAccount := v.(*stream.StreamsForAccount) //nolint:forcetypeassert
streamsForAccount := v.(*stream.StreamsForAccount)
streamsForAccount.Lock()
defer streamsForAccount.Unlock()

View File

@@ -147,27 +147,27 @@ func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) er
return nil
}
// Populate model.
// Ensure the status model is fully populated.
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("error populating status: %w", err)
}
// Parse relevant URI(s).
// Parse the outbox URI of the status author.
outboxIRI, err := parseURI(status.Account.OutboxURI)
if err != nil {
return err
}
// Convert status to an ActivityStreams
// Note, wrapped in a Create activity.
asStatus, err := f.converter.StatusToAS(ctx, status)
// Convert status to ActivityStreams Statusable implementing type.
statusable, err := f.converter.StatusToAS(ctx, status)
if err != nil {
return gtserror.Newf("error converting status to AS: %w", err)
return gtserror.Newf("error converting status to Statusable: %w", err)
}
create, err := f.converter.WrapNoteInCreate(asStatus, false)
// Use ActivityStreams Statusable type as Object of Create.
create, err := f.converter.WrapStatusableInCreate(statusable, false)
if err != nil {
return gtserror.Newf("error wrapping status in create: %w", err)
return gtserror.Newf("error wrapping Statusable in Create: %w", err)
}
// Send the Create via the Actor's outbox.
@@ -196,12 +196,12 @@ func (f *federate) DeleteStatus(ctx context.Context, status *gtsmodel.Status) er
return nil
}
// Populate model.
// Ensure the status model is fully populated.
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("error populating status: %w", err)
}
// Parse relevant URI(s).
// Parse the outbox URI of the status author.
outboxIRI, err := parseURI(status.Account.OutboxURI)
if err != nil {
return err
@@ -226,6 +226,50 @@ func (f *federate) DeleteStatus(ctx context.Context, status *gtsmodel.Status) er
return nil
}
func (f *federate) UpdateStatus(ctx context.Context, status *gtsmodel.Status) error {
// Do nothing if the status
// shouldn't be federated.
if !*status.Federated {
return nil
}
// Do nothing if this
// isn't our status.
if !*status.Local {
return nil
}
// Ensure the status model is fully populated.
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("error populating status: %w", err)
}
// Parse the outbox URI of the status author.
outboxIRI, err := parseURI(status.Account.OutboxURI)
if err != nil {
return err
}
// Convert status to ActivityStreams Statusable implementing type.
statusable, err := f.converter.StatusToAS(ctx, status)
if err != nil {
return gtserror.Newf("error converting status to Statusable: %w", err)
}
// Use ActivityStreams Statusable type as Object of Update.
update, err := f.converter.WrapStatusableInUpdate(statusable, false)
if err != nil {
return gtserror.Newf("error wrapping Statusable in Update: %w", err)
}
// Send the Update activity with Statusable via the Actor's outbox.
if _, err := f.FederatingActor().Send(ctx, outboxIRI, update); err != nil {
return gtserror.Newf("error sending Update activity via outbox %s: %w", outboxIRI, err)
}
return nil
}
func (f *federate) Follow(ctx context.Context, follow *gtsmodel.Follow) error {
// Populate model.
if err := f.state.DB.PopulateFollow(ctx, follow); err != nil {

View File

@@ -114,6 +114,10 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg messages.From
case ap.ActivityUpdate:
switch cMsg.APObjectType {
// UPDATE NOTE/STATUS
case ap.ObjectNote:
return p.clientAPI.UpdateStatus(ctx, cMsg)
// UPDATE PROFILE/ACCOUNT
case ap.ObjectProfile, ap.ActorPerson:
return p.clientAPI.UpdateAccount(ctx, cMsg)
@@ -332,10 +336,25 @@ func (p *clientAPI) CreateBlock(ctx context.Context, cMsg messages.FromClientAPI
return nil
}
func (p *clientAPI) UpdateStatus(ctx context.Context, cMsg messages.FromClientAPI) error {
// Cast the updated Status model attached to msg.
status, ok := cMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("cannot cast %T -> *gtsmodel.Status", cMsg.GTSModel)
}
// Federate the updated status changes out remotely.
if err := p.federate.UpdateStatus(ctx, status); err != nil {
return gtserror.Newf("error federating status update: %w", err)
}
return nil
}
func (p *clientAPI) UpdateAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
account, ok := cMsg.GTSModel.(*gtsmodel.Account)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Account", cMsg.GTSModel)
return gtserror.Newf("cannot cast %T -> *gtsmodel.Account", cMsg.GTSModel)
}
if err := p.federate.UpdateAccount(ctx, account); err != nil {

View File

@@ -119,6 +119,10 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg messages.FromFe
case ap.ActivityUpdate:
switch fMsg.APObjectType { //nolint:gocritic
// UPDATE NOTE/STATUS
case ap.ObjectNote:
return p.fediAPI.UpdateStatus(ctx, fMsg)
// UPDATE PROFILE/ACCOUNT
case ap.ObjectProfile:
return p.fediAPI.UpdateAccount(ctx, fMsg)
@@ -485,13 +489,13 @@ func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg messages.FromFediAPI)
// Parse the old/existing account model.
account, ok := fMsg.GTSModel.(*gtsmodel.Account)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Account", fMsg.GTSModel)
return gtserror.Newf("cannot cast %T -> *gtsmodel.Account", fMsg.GTSModel)
}
// Because this was an Update, the new Accountable should be set on the message.
apubAcc, ok := fMsg.APObjectModel.(ap.Accountable)
if !ok {
return gtserror.Newf("%T not parseable as ap.Accountable", fMsg.APObjectModel)
return gtserror.Newf("cannot cast %T -> ap.Accountable", fMsg.APObjectModel)
}
// Fetch up-to-date bio, avatar, header, etc.
@@ -509,6 +513,34 @@ func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg messages.FromFediAPI)
return nil
}
func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg messages.FromFediAPI) error {
// Cast the existing Status model attached to msg.
existing, ok := fMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("cannot cast %T -> *gtsmodel.Status", fMsg.GTSModel)
}
// Cast the updated ActivityPub statusable object .
apStatus, ok := fMsg.APObjectModel.(ap.Statusable)
if !ok {
return gtserror.Newf("cannot cast %T -> ap.Statusable", fMsg.APObjectModel)
}
// Fetch up-to-date attach status attachments, etc.
_, _, err := p.federate.RefreshStatus(
ctx,
fMsg.ReceivingAccount.Username,
existing,
apStatus,
false,
)
if err != nil {
return gtserror.Newf("error refreshing updated status: %w", err)
}
return nil
}
func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) error {
// Delete attachments from this status, since this request
// comes from the federating API, and there's no way the

View File

@@ -38,7 +38,7 @@ func wipeStatusF(state *state.State, media *media.Processor, surface *surface) w
statusToDelete *gtsmodel.Status,
deleteAttachments bool,
) error {
errs := new(gtserror.MultiError)
var errs gtserror.MultiError
// Either delete all attachments for this status,
// or simply unattach + clean them separately later.
@@ -48,15 +48,15 @@ func wipeStatusF(state *state.State, media *media.Processor, surface *surface) w
// status immediately (in case of delete + redraft)
if deleteAttachments {
// todo:state.DB.DeleteAttachmentsForStatus
for _, a := range statusToDelete.AttachmentIDs {
if err := media.Delete(ctx, a); err != nil {
for _, id := range statusToDelete.AttachmentIDs {
if err := media.Delete(ctx, id); err != nil {
errs.Appendf("error deleting media: %w", err)
}
}
} else {
// todo:state.DB.UnattachAttachmentsForStatus
for _, a := range statusToDelete.AttachmentIDs {
if _, err := media.Unattach(ctx, statusToDelete.Account, a); err != nil {
for _, id := range statusToDelete.AttachmentIDs {
if _, err := media.Unattach(ctx, statusToDelete.Account, id); err != nil {
errs.Appendf("error unattaching media: %w", err)
}
}
@@ -95,11 +95,12 @@ func wipeStatusF(state *state.State, media *media.Processor, surface *surface) w
if err != nil {
errs.Appendf("error fetching status boosts: %w", err)
}
for _, b := range boosts {
if err := surface.deleteStatusFromTimelines(ctx, b.ID); err != nil {
for _, boost := range boosts {
if err := surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil {
errs.Appendf("error deleting boost from timelines: %w", err)
}
if err := state.DB.DeleteStatusByID(ctx, b.ID); err != nil {
if err := state.DB.DeleteStatusByID(ctx, boost.ID); err != nil {
errs.Appendf("error deleting boost: %w", err)
}
}