mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-02 15:18:07 -06:00
[feature] add support for receiving federated status edits (#3597)
* add support for extracting Updated field from Statusable implementers
* add support for status edits in the database, and update status dereferencer to handle them
* remove unused AdditionalInfo{}.CreatedAt
* remove unused AdditionalEmojiInfo{}.CreatedAt
* update new mention creation to use status.UpdatedAt
* remove mention.UpdatedAt, fixes related to NewULIDFromTime() change
* add migration to remove Mention{}.UpdatedAt field
* add migration to add the StatusEdit{} table
* start adding tests, add delete function for status edits
* add more of status edit migrations, fill in more of the necessary edit delete functionality
* remove unused function
* allow generating gotosocial compatible ulid via CLI with `go run ./cmd/gen-ulid`
* add StatusEdit{} test models
* fix new statusedits sql
* use model instead of table name
* actually remove the Mention.UpdatedAt field...
* fix tests now new models are added, add more status edit DB tests
* fix panic wording
* add test for deleting status edits
* don't automatically set `updated_at` field on updated statuses
* flesh out more of the dereferencer status edit tests, ensure updated at field set on outgoing AS statuses
* remove media_attachments.updated_at column
* fix up more tests, further complete the dereferencer status edit tests
* update more status serialization tests not expecting 'updated' AS property
* gah!! json serialization tests!!
* undo some gtscontext wrapping changes
* more serialization test fixing 🥲
* more test fixing, ensure the edit.status_id field is actually set 🤦
* fix status edit test
* grrr linter
* add edited_at field to apimodel status
* remove the choice of paging on the timeline public filtered test (otherwise it needs updating every time you add statuses ...)
* ensure that status.updated_at always fits chronologically
* fix more serialization tests ...
* add more code comments
* fix envparsing
* update swagger file
* properly handle media description changes during status edits
* slight formatting tweak
* code comment
This commit is contained in:
parent
3e18d97a6e
commit
23fc70f4e6
86 changed files with 2557 additions and 651 deletions
|
|
@ -87,7 +87,7 @@ func (d *Dereferencer) EnrichAnnounce(
|
|||
boost.Federated = target.Federated
|
||||
|
||||
// Ensure this Announce is permitted by the Announcee.
|
||||
permit, err := d.isPermittedStatus(ctx, requestUser, nil, boost)
|
||||
permit, err := d.isPermittedStatus(ctx, requestUser, nil, boost, true)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error checking permitted status %s: %w", boost.URI, err)
|
||||
}
|
||||
|
|
@ -99,10 +99,7 @@ func (d *Dereferencer) EnrichAnnounce(
|
|||
}
|
||||
|
||||
// Generate an ID for the boost wrapper status.
|
||||
boost.ID, err = id.NewULIDFromTime(boost.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error generating id: %w", err)
|
||||
}
|
||||
boost.ID = id.NewULIDFromTime(boost.CreatedAt)
|
||||
|
||||
// Store the boost wrapper status in database.
|
||||
switch err = d.state.DB.PutStatus(ctx, boost); {
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ func (d *Dereferencer) RefreshMedia(
|
|||
// Check emoji is up-to-date
|
||||
// with provided extra info.
|
||||
switch {
|
||||
case force:
|
||||
case info.Blurhash != nil &&
|
||||
*info.Blurhash != attach.Blurhash:
|
||||
attach.Blurhash = *info.Blurhash
|
||||
|
|
|
|||
|
|
@ -302,6 +302,7 @@ func (d *Dereferencer) enrichStatusSafely(
|
|||
uri,
|
||||
status,
|
||||
statusable,
|
||||
isNew,
|
||||
)
|
||||
|
||||
// Check for a returned HTTP code via error.
|
||||
|
|
@ -374,6 +375,7 @@ func (d *Dereferencer) enrichStatus(
|
|||
uri *url.URL,
|
||||
status *gtsmodel.Status,
|
||||
statusable ap.Statusable,
|
||||
isNew bool,
|
||||
) (
|
||||
*gtsmodel.Status,
|
||||
ap.Statusable,
|
||||
|
|
@ -476,8 +478,7 @@ func (d *Dereferencer) enrichStatus(
|
|||
|
||||
// Ensure the final parsed status URI or URL matches
|
||||
// the input URI we fetched (or received) it as.
|
||||
matches, err := util.URIMatches(
|
||||
uri,
|
||||
matches, err := util.URIMatches(uri,
|
||||
append(
|
||||
ap.GetURL(statusable), // status URL(s)
|
||||
ap.GetJSONLDId(statusable), // status URI
|
||||
|
|
@ -497,19 +498,10 @@ func (d *Dereferencer) enrichStatus(
|
|||
)
|
||||
}
|
||||
|
||||
var isNew bool
|
||||
|
||||
// Based on the original provided
|
||||
// status model, determine whether
|
||||
// this is a new insert / update.
|
||||
if isNew = (status.ID == ""); isNew {
|
||||
if isNew {
|
||||
|
||||
// Generate new status ID from the provided creation date.
|
||||
latestStatus.ID, err = id.NewULIDFromTime(latestStatus.CreatedAt)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "invalid created at date (falling back to 'now'): %v", err)
|
||||
latestStatus.ID = id.NewULID() // just use "now"
|
||||
}
|
||||
latestStatus.ID = id.NewULIDFromTime(latestStatus.CreatedAt)
|
||||
} else {
|
||||
|
||||
// Reuse existing status ID.
|
||||
|
|
@ -519,7 +511,6 @@ func (d *Dereferencer) enrichStatus(
|
|||
// Set latest fetch time and carry-
|
||||
// over some values from "old" status.
|
||||
latestStatus.FetchedAt = time.Now()
|
||||
latestStatus.UpdatedAt = status.UpdatedAt
|
||||
latestStatus.Local = status.Local
|
||||
latestStatus.PinnedAt = status.PinnedAt
|
||||
|
||||
|
|
@ -538,8 +529,9 @@ func (d *Dereferencer) enrichStatus(
|
|||
}
|
||||
|
||||
// Check if this is a permitted status we should accept.
|
||||
// Function also sets "PendingApproval" bool as necessary.
|
||||
permit, err := d.isPermittedStatus(ctx, requestUser, status, latestStatus)
|
||||
// Function also sets "PendingApproval" bool as necessary,
|
||||
// and handles removal of existing statuses no longer permitted.
|
||||
permit, err := d.isPermittedStatus(ctx, requestUser, status, latestStatus, isNew)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error checking permissibility for status %s: %w", uri, err)
|
||||
}
|
||||
|
|
@ -550,59 +542,113 @@ func (d *Dereferencer) enrichStatus(
|
|||
return nil, nil, gtserror.SetNotPermitted(err)
|
||||
}
|
||||
|
||||
// Ensure the status' mentions are populated, and pass in existing to check for changes.
|
||||
if err := d.fetchStatusMentions(ctx, requestUser, status, latestStatus); err != nil {
|
||||
// Insert / update any attached status poll.
|
||||
pollChanged, err := d.handleStatusPoll(ctx,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error handling poll for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Populate mentions associated with status, passing
|
||||
// in existing status to reuse old where possible.
|
||||
// (especially important here to reduce need to dereference).
|
||||
mentionsChanged, err := d.fetchStatusMentions(ctx,
|
||||
requestUser,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating mentions for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Ensure the status' poll remains consistent, else reset the poll.
|
||||
if err := d.fetchStatusPoll(ctx, status, latestStatus); err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating poll for status %s: %w", uri, err)
|
||||
// Ensure status in a thread is connected.
|
||||
threadChanged, err := d.threadStatus(ctx,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error handling threading for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Now that we know who this status replies to (handled by ASStatusToStatus)
|
||||
// and who it mentions, we can add a ThreadID to it if necessary.
|
||||
if err := d.threadStatus(ctx, latestStatus); err != nil {
|
||||
return nil, nil, gtserror.Newf("error checking / creating threadID for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Ensure the status' tags are populated, (changes are expected / okay).
|
||||
if err := d.fetchStatusTags(ctx, status, latestStatus); err != nil {
|
||||
// Populate tags associated with status, passing
|
||||
// in existing status to reuse old where possible.
|
||||
tagsChanged, err := d.fetchStatusTags(ctx,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating tags for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Ensure the status' media attachments are populated, passing in existing to check for changes.
|
||||
if err := d.fetchStatusAttachments(ctx, requestUser, status, latestStatus); err != nil {
|
||||
// Populate media attachments associated with status,
|
||||
// passing in existing status to reuse old where possible
|
||||
// (especially important here to reduce need to dereference).
|
||||
mediaChanged, err := d.fetchStatusAttachments(ctx,
|
||||
requestUser,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating attachments for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Ensure the status' emoji attachments are populated, passing in existing to check for changes.
|
||||
if err := d.fetchStatusEmojis(ctx, status, latestStatus); err != nil {
|
||||
// Populate emoji associated with status, passing
|
||||
// in existing status to reuse old where possible
|
||||
// (especially important here to reduce need to dereference).
|
||||
emojiChanged, err := d.fetchStatusEmojis(ctx,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
if isNew {
|
||||
// This is new, put the status in the database.
|
||||
err := d.state.DB.PutStatus(ctx, latestStatus)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error putting in database: %w", err)
|
||||
// Simplest case, insert this new status into the database.
|
||||
if err := d.state.DB.PutStatus(ctx, latestStatus); err != nil {
|
||||
return nil, nil, gtserror.Newf("error inserting new status %s: %w", uri, err)
|
||||
}
|
||||
} else {
|
||||
// This is an existing status, update the model in the database.
|
||||
if err := d.state.DB.UpdateStatus(ctx, latestStatus); err != nil {
|
||||
return nil, nil, gtserror.Newf("error updating database: %w", err)
|
||||
// Check for and handle any edits to status, inserting
|
||||
// historical edit if necessary. Also determines status
|
||||
// columns that need updating in below query.
|
||||
cols, err := d.handleStatusEdit(ctx,
|
||||
status,
|
||||
latestStatus,
|
||||
pollChanged,
|
||||
mentionsChanged,
|
||||
threadChanged,
|
||||
tagsChanged,
|
||||
mediaChanged,
|
||||
emojiChanged,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error handling edit for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// With returned changed columns, now update the existing status entry.
|
||||
if err := d.state.DB.UpdateStatus(ctx, latestStatus, cols...); err != nil {
|
||||
return nil, nil, gtserror.Newf("error updating existing status %s: %w", uri, err)
|
||||
}
|
||||
}
|
||||
|
||||
return latestStatus, statusable, nil
|
||||
}
|
||||
|
||||
// fetchStatusMentions populates the mentions on 'status', creating
|
||||
// new where needed, or using unchanged mentions from 'existing' status.
|
||||
func (d *Dereferencer) fetchStatusMentions(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
|
||||
// Allocate new slice to take the yet-to-be created mention IDs.
|
||||
status.MentionIDs = make([]string, len(status.Mentions))
|
||||
|
||||
|
|
@ -610,7 +656,6 @@ func (d *Dereferencer) fetchStatusMentions(
|
|||
var (
|
||||
mention = status.Mentions[i]
|
||||
alreadyExists bool
|
||||
err error
|
||||
)
|
||||
|
||||
// Search existing status for a mention already stored,
|
||||
|
|
@ -633,19 +678,16 @@ func (d *Dereferencer) fetchStatusMentions(
|
|||
continue
|
||||
}
|
||||
|
||||
// Mark status as
|
||||
// having changed.
|
||||
changed = true
|
||||
|
||||
// This mention didn't exist yet.
|
||||
// Generate new ID according to status creation.
|
||||
// TODO: update this to use "edited_at" when we add
|
||||
// support for edited status revision history.
|
||||
mention.ID, err = id.NewULIDFromTime(status.CreatedAt)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "invalid created at date (falling back to 'now'): %v", err)
|
||||
mention.ID = id.NewULID() // just use "now"
|
||||
}
|
||||
// Generate new ID according to latest update.
|
||||
mention.ID = id.NewULIDFromTime(status.UpdatedAt)
|
||||
|
||||
// Set known further mention details.
|
||||
mention.CreatedAt = status.CreatedAt
|
||||
mention.UpdatedAt = status.UpdatedAt
|
||||
mention.CreatedAt = status.UpdatedAt
|
||||
mention.OriginAccount = status.Account
|
||||
mention.OriginAccountID = status.AccountID
|
||||
mention.OriginAccountURI = status.AccountURI
|
||||
|
|
@ -657,7 +699,7 @@ func (d *Dereferencer) fetchStatusMentions(
|
|||
|
||||
// Place the new mention into the database.
|
||||
if err := d.state.DB.PutMention(ctx, mention); err != nil {
|
||||
return gtserror.Newf("error putting mention in database: %w", err)
|
||||
return changed, gtserror.Newf("error putting mention in database: %w", err)
|
||||
}
|
||||
|
||||
// Set the *new* mention and ID.
|
||||
|
|
@ -678,17 +720,42 @@ func (d *Dereferencer) fetchStatusMentions(
|
|||
i++
|
||||
}
|
||||
|
||||
return nil
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||
if status.InReplyTo != nil {
|
||||
if parentThreadID := status.InReplyTo.ThreadID; parentThreadID != "" {
|
||||
// Simplest case: parent status
|
||||
// is threaded, so inherit threadID.
|
||||
status.ThreadID = parentThreadID
|
||||
return nil
|
||||
// threadStatus ensures that given status is threaded correctly
|
||||
// where necessary. that is it will inherit a thread ID from the
|
||||
// existing copy if it is threaded correctly, else it will inherit
|
||||
// a thread ID from a parent with existing thread, else it will
|
||||
// generate a new thread ID if status mentions a local account.
|
||||
func (d *Dereferencer) threadStatus(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
|
||||
// Check for existing status
|
||||
// that is already threaded.
|
||||
if existing.ThreadID != "" {
|
||||
|
||||
// Existing is threaded correctly.
|
||||
if existing.InReplyTo == nil ||
|
||||
existing.InReplyTo.ThreadID == existing.ThreadID {
|
||||
status.ThreadID = existing.ThreadID
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// TODO: delete incorrect thread
|
||||
}
|
||||
|
||||
// Check for existing parent to inherit threading from.
|
||||
if inReplyTo := status.InReplyTo; inReplyTo != nil &&
|
||||
inReplyTo.ThreadID != "" {
|
||||
status.ThreadID = inReplyTo.ThreadID
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Parent wasn't threaded. If this
|
||||
|
|
@ -711,7 +778,7 @@ func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status
|
|||
// Status doesn't mention a
|
||||
// local account, so we don't
|
||||
// need to thread it.
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Status mentions a local account.
|
||||
|
|
@ -719,24 +786,30 @@ func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status
|
|||
// it to the status.
|
||||
threadID := id.NewULID()
|
||||
|
||||
if err := d.state.DB.PutThread(
|
||||
ctx,
|
||||
>smodel.Thread{
|
||||
ID: threadID,
|
||||
},
|
||||
// Insert new thread model into db.
|
||||
if err := d.state.DB.PutThread(ctx,
|
||||
>smodel.Thread{ID: threadID},
|
||||
); err != nil {
|
||||
return gtserror.Newf("error inserting new thread in db: %w", err)
|
||||
return false, gtserror.Newf("error inserting new thread in db: %w", err)
|
||||
}
|
||||
|
||||
// Set thread on latest status.
|
||||
status.ThreadID = threadID
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// fetchStatusTags populates the tags on 'status', fetching existing
|
||||
// from the database and creating new where needed. 'existing' is used
|
||||
// to fetch tags that have not changed since previous stored status.
|
||||
func (d *Dereferencer) fetchStatusTags(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
|
||||
// Allocate new slice to take the yet-to-be determined tag IDs.
|
||||
status.TagIDs = make([]string, len(status.Tags))
|
||||
|
||||
|
|
@ -751,10 +824,14 @@ func (d *Dereferencer) fetchStatusTags(
|
|||
continue
|
||||
}
|
||||
|
||||
// Mark status as
|
||||
// having changed.
|
||||
changed = true
|
||||
|
||||
// Look for existing tag with name in the database.
|
||||
existing, err := d.state.DB.GetTagByName(ctx, tag.Name)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return gtserror.Newf("db error getting tag %s: %w", tag.Name, err)
|
||||
return changed, gtserror.Newf("db error getting tag %s: %w", tag.Name, err)
|
||||
} else if existing != nil {
|
||||
status.Tags[i] = existing
|
||||
status.TagIDs[i] = existing.ID
|
||||
|
|
@ -788,106 +865,21 @@ func (d *Dereferencer) fetchStatusTags(
|
|||
i++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dereferencer) fetchStatusPoll(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
var (
|
||||
// insertStatusPoll generates ID and inserts the poll attached to status into the database.
|
||||
insertStatusPoll = func(ctx context.Context, status *gtsmodel.Status) error {
|
||||
var err error
|
||||
|
||||
// Generate new ID for poll from the status CreatedAt.
|
||||
// TODO: update this to use "edited_at" when we add
|
||||
// support for edited status revision history.
|
||||
status.Poll.ID, err = id.NewULIDFromTime(status.CreatedAt)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "invalid created at date (falling back to 'now'): %v", err)
|
||||
status.Poll.ID = id.NewULID() // just use "now"
|
||||
}
|
||||
|
||||
// Update the status<->poll links.
|
||||
status.PollID = status.Poll.ID
|
||||
status.Poll.StatusID = status.ID
|
||||
status.Poll.Status = status
|
||||
|
||||
// Insert this latest poll into the database.
|
||||
err = d.state.DB.PutPoll(ctx, status.Poll)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error putting in database: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteStatusPoll deletes the poll with ID, and all attached votes, from the database.
|
||||
deleteStatusPoll = func(ctx context.Context, pollID string) error {
|
||||
if err := d.state.DB.DeletePollByID(ctx, pollID); err != nil {
|
||||
return gtserror.Newf("error deleting existing poll from database: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
|
||||
switch {
|
||||
case existing.Poll == nil && status.Poll == nil:
|
||||
// no poll before or after, nothing to do.
|
||||
return nil
|
||||
|
||||
case existing.Poll == nil && status.Poll != nil:
|
||||
// no previous poll, insert new poll!
|
||||
return insertStatusPoll(ctx, status)
|
||||
|
||||
case status.Poll == nil:
|
||||
// existing poll has been deleted, remove this.
|
||||
return deleteStatusPoll(ctx, existing.PollID)
|
||||
|
||||
case pollChanged(existing.Poll, status.Poll):
|
||||
// poll has changed since original, delete and reinsert new.
|
||||
if err := deleteStatusPoll(ctx, existing.PollID); err != nil {
|
||||
return err
|
||||
}
|
||||
return insertStatusPoll(ctx, status)
|
||||
|
||||
case pollUpdated(existing.Poll, status.Poll):
|
||||
// Since we last saw it, the poll has updated!
|
||||
// Whether that be stats, or close time.
|
||||
poll := existing.Poll
|
||||
poll.Closing = pollJustClosed(existing.Poll, status.Poll)
|
||||
poll.ClosedAt = status.Poll.ClosedAt
|
||||
poll.Voters = status.Poll.Voters
|
||||
poll.Votes = status.Poll.Votes
|
||||
|
||||
// Update poll model in the database (specifically only the possible changed columns).
|
||||
if err := d.state.DB.UpdatePoll(ctx, poll, "closed_at", "voters", "votes"); err != nil {
|
||||
return gtserror.Newf("error updating poll: %w", err)
|
||||
}
|
||||
|
||||
// Update poll on status.
|
||||
status.PollID = poll.ID
|
||||
status.Poll = poll
|
||||
return nil
|
||||
|
||||
default:
|
||||
// latest and existing
|
||||
// polls are up to date.
|
||||
poll := existing.Poll
|
||||
status.PollID = poll.ID
|
||||
status.Poll = poll
|
||||
return nil
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// fetchStatusAttachments populates the attachments on 'status', creating new database
|
||||
// entries where needed and dereferencing it, or using unchanged from 'existing' status.
|
||||
func (d *Dereferencer) fetchStatusAttachments(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
|
||||
// Allocate new slice to take the yet-to-be fetched attachment IDs.
|
||||
status.AttachmentIDs = make([]string, len(status.Attachments))
|
||||
|
||||
|
|
@ -897,9 +889,26 @@ func (d *Dereferencer) fetchStatusAttachments(
|
|||
// Look for existing media attachment with remote URL first.
|
||||
existing, ok := existing.GetAttachmentByRemoteURL(placeholder.RemoteURL)
|
||||
if ok && existing.ID != "" {
|
||||
var info media.AdditionalMediaInfo
|
||||
|
||||
// Ensure the existing media attachment is up-to-date and cached.
|
||||
existing, err := d.updateAttachment(ctx, requestUser, existing, placeholder)
|
||||
// Look for any difference in stored media description.
|
||||
diff := (existing.Description != placeholder.Description)
|
||||
if diff {
|
||||
info.Description = &placeholder.Description
|
||||
}
|
||||
|
||||
// If description changed,
|
||||
// we mark media as changed.
|
||||
changed = changed || diff
|
||||
|
||||
// Store any attachment updates and
|
||||
// ensure media is locally cached.
|
||||
existing, err := d.RefreshMedia(ctx,
|
||||
requestUser,
|
||||
existing,
|
||||
info,
|
||||
diff,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error updating existing attachment: %v", err)
|
||||
|
||||
|
|
@ -915,9 +924,12 @@ func (d *Dereferencer) fetchStatusAttachments(
|
|||
continue
|
||||
}
|
||||
|
||||
// Mark status as
|
||||
// having changed.
|
||||
changed = true
|
||||
|
||||
// Load this new media attachment.
|
||||
attachment, err := d.GetMedia(
|
||||
ctx,
|
||||
attachment, err := d.GetMedia(ctx,
|
||||
requestUser,
|
||||
status.AccountID,
|
||||
placeholder.RemoteURL,
|
||||
|
|
@ -955,28 +967,34 @@ func (d *Dereferencer) fetchStatusAttachments(
|
|||
i++
|
||||
}
|
||||
|
||||
return nil
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// fetchStatusEmojis populates the emojis on 'status', creating new database entries
|
||||
// where needed and dereferencing it, or using unchanged from 'existing' status.
|
||||
func (d *Dereferencer) fetchStatusEmojis(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
|
||||
// Fetch the updated emojis for our status.
|
||||
emojis, changed, err := d.fetchEmojis(ctx,
|
||||
existing.Emojis,
|
||||
status.Emojis,
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error fetching emojis: %w", err)
|
||||
return changed, gtserror.Newf("error fetching emojis: %w", err)
|
||||
}
|
||||
|
||||
if !changed {
|
||||
// Use existing status emoji objects.
|
||||
status.EmojiIDs = existing.EmojiIDs
|
||||
status.Emojis = existing.Emojis
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Set latest emojis.
|
||||
|
|
@ -988,9 +1006,254 @@ func (d *Dereferencer) fetchStatusEmojis(
|
|||
status.EmojiIDs[i] = emoji.ID
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// handleStatusPoll handles both inserting of new status poll or the
|
||||
// update of an existing poll. this handles the case of simple vote
|
||||
// count updates (without being classified as a change of the poll
|
||||
// itself), as well as full poll changes that delete existing instance.
|
||||
func (d *Dereferencer) handleStatusPoll(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
switch {
|
||||
case existing.Poll == nil && status.Poll == nil:
|
||||
// no poll before or after, nothing to do.
|
||||
return false, nil
|
||||
|
||||
case existing.Poll == nil && status.Poll != nil:
|
||||
// no previous poll, insert new status poll!
|
||||
return true, d.insertStatusPoll(ctx, status)
|
||||
|
||||
case status.Poll == nil:
|
||||
// existing status poll has been deleted, remove this from the database.
|
||||
if err = d.state.DB.DeletePollByID(ctx, existing.Poll.ID); err != nil {
|
||||
err = gtserror.Newf("error deleting poll from database: %w", err)
|
||||
}
|
||||
return true, err
|
||||
|
||||
case pollChanged(existing.Poll, status.Poll):
|
||||
// existing status poll has been changed, remove this from the database.
|
||||
if err = d.state.DB.DeletePollByID(ctx, existing.Poll.ID); err != nil {
|
||||
return true, gtserror.Newf("error deleting poll from database: %w", err)
|
||||
}
|
||||
|
||||
// insert latest poll version into database.
|
||||
return true, d.insertStatusPoll(ctx, status)
|
||||
|
||||
case pollStateUpdated(existing.Poll, status.Poll):
|
||||
// Since we last saw it, the poll has updated!
|
||||
// Whether that be stats, or close time.
|
||||
poll := existing.Poll
|
||||
poll.Closing = pollJustClosed(existing.Poll, status.Poll)
|
||||
poll.ClosedAt = status.Poll.ClosedAt
|
||||
poll.Voters = status.Poll.Voters
|
||||
poll.Votes = status.Poll.Votes
|
||||
|
||||
// Update poll model in the database (specifically only the possible changed columns).
|
||||
if err = d.state.DB.UpdatePoll(ctx, poll, "closed_at", "voters", "votes"); err != nil {
|
||||
return false, gtserror.Newf("error updating poll: %w", err)
|
||||
}
|
||||
|
||||
// Update poll on status.
|
||||
status.PollID = poll.ID
|
||||
status.Poll = poll
|
||||
return false, nil
|
||||
|
||||
default:
|
||||
// latest and existing
|
||||
// polls are up to date.
|
||||
poll := existing.Poll
|
||||
status.PollID = poll.ID
|
||||
status.Poll = poll
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// insertStatusPoll inserts an assumed new poll attached to status into the database, this
|
||||
// also handles generating new ID for the poll and setting necessary fields on the status.
|
||||
func (d *Dereferencer) insertStatusPoll(ctx context.Context, status *gtsmodel.Status) error {
|
||||
var err error
|
||||
|
||||
// Generate new ID for poll from latest updated time.
|
||||
status.Poll.ID = id.NewULIDFromTime(status.UpdatedAt)
|
||||
|
||||
// Update the status<->poll links.
|
||||
status.PollID = status.Poll.ID
|
||||
status.Poll.StatusID = status.ID
|
||||
status.Poll.Status = status
|
||||
|
||||
// Insert this latest poll into the database.
|
||||
err = d.state.DB.PutPoll(ctx, status.Poll)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error putting poll in database: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleStatusEdit compiles a list of changed status table columns between
|
||||
// existing and latest status model, and where necessary inserts a historic
|
||||
// edit of the status into the database to store its previous state. the
|
||||
// returned slice is a list of columns requiring updating in the database.
|
||||
func (d *Dereferencer) handleStatusEdit(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
pollChanged bool,
|
||||
mentionsChanged bool,
|
||||
threadChanged bool,
|
||||
tagsChanged bool,
|
||||
mediaChanged bool,
|
||||
emojiChanged bool,
|
||||
) (
|
||||
cols []string,
|
||||
err error,
|
||||
) {
|
||||
var edited bool
|
||||
|
||||
// Preallocate max slice length.
|
||||
cols = make([]string, 0, 13)
|
||||
|
||||
// Always update `fetched_at`.
|
||||
cols = append(cols, "fetched_at")
|
||||
|
||||
// Check for edited status content.
|
||||
if existing.Content != status.Content {
|
||||
cols = append(cols, "content")
|
||||
edited = true
|
||||
}
|
||||
|
||||
// Check for edited status content warning.
|
||||
if existing.ContentWarning != status.ContentWarning {
|
||||
cols = append(cols, "content_warning")
|
||||
edited = true
|
||||
}
|
||||
|
||||
// Check for edited status sensitive flag.
|
||||
if *existing.Sensitive != *status.Sensitive {
|
||||
cols = append(cols, "sensitive")
|
||||
edited = true
|
||||
}
|
||||
|
||||
// Check for edited status language tag.
|
||||
if existing.Language != status.Language {
|
||||
cols = append(cols, "language")
|
||||
edited = true
|
||||
}
|
||||
|
||||
if pollChanged {
|
||||
// Attached poll was changed.
|
||||
cols = append(cols, "poll_id")
|
||||
edited = true
|
||||
}
|
||||
|
||||
if mentionsChanged {
|
||||
cols = append(cols, "mentions") // i.e. MentionIDs
|
||||
|
||||
// Mentions changed doesn't necessarily
|
||||
// indicate an edit, it may just not have
|
||||
// been previously populated properly.
|
||||
}
|
||||
|
||||
if threadChanged {
|
||||
cols = append(cols, "thread_id")
|
||||
|
||||
// Thread changed doesn't necessarily
|
||||
// indicate an edit, it may just now
|
||||
// actually be included in a thread.
|
||||
}
|
||||
|
||||
if tagsChanged {
|
||||
cols = append(cols, "tags") // i.e. TagIDs
|
||||
|
||||
// Tags changed doesn't necessarily
|
||||
// indicate an edit, it may just not have
|
||||
// been previously populated properly.
|
||||
}
|
||||
|
||||
if mediaChanged {
|
||||
// Attached media was changed.
|
||||
cols = append(cols, "attachments") // i.e. AttachmentIDs
|
||||
edited = true
|
||||
}
|
||||
|
||||
if emojiChanged {
|
||||
// Attached emojis changed.
|
||||
cols = append(cols, "emojis") // i.e. EmojiIDs
|
||||
|
||||
// Emojis changed doesn't necessarily
|
||||
// indicate an edit, it may just not have
|
||||
// been previously populated properly.
|
||||
}
|
||||
|
||||
if edited {
|
||||
// We prefer to use provided 'upated_at', but ensure
|
||||
// it fits chronologically with creation / last update.
|
||||
if !status.UpdatedAt.After(status.CreatedAt) ||
|
||||
!status.UpdatedAt.After(existing.UpdatedAt) {
|
||||
|
||||
// Else fallback to now as update time.
|
||||
status.UpdatedAt = status.FetchedAt
|
||||
}
|
||||
|
||||
// Status has been editted since last
|
||||
// we saw it, take snapshot of existing.
|
||||
var edit gtsmodel.StatusEdit
|
||||
edit.ID = id.NewULIDFromTime(status.UpdatedAt)
|
||||
edit.Content = existing.Content
|
||||
edit.ContentWarning = existing.ContentWarning
|
||||
edit.Text = existing.Text
|
||||
edit.Language = existing.Language
|
||||
edit.Sensitive = existing.Sensitive
|
||||
edit.StatusID = status.ID
|
||||
|
||||
// Copy existing attachments and descriptions.
|
||||
edit.AttachmentIDs = existing.AttachmentIDs
|
||||
edit.Attachments = existing.Attachments
|
||||
if l := len(existing.Attachments); l > 0 {
|
||||
edit.AttachmentDescriptions = make([]string, l)
|
||||
for i, attach := range existing.Attachments {
|
||||
edit.AttachmentDescriptions[i] = attach.Description
|
||||
}
|
||||
}
|
||||
|
||||
// Edit creation is last update time.
|
||||
edit.CreatedAt = existing.UpdatedAt
|
||||
|
||||
if existing.Poll != nil {
|
||||
// Poll only set if existing contained them.
|
||||
edit.PollOptions = existing.Poll.Options
|
||||
|
||||
if !*existing.Poll.HideCounts || pollChanged {
|
||||
// If the counts are allowed to be
|
||||
// shown, or poll has changed, then
|
||||
// include poll vote counts in edit.
|
||||
edit.PollVotes = existing.Poll.Votes
|
||||
}
|
||||
}
|
||||
|
||||
// Insert this new edit of existing status into database.
|
||||
if err := d.state.DB.PutStatusEdit(ctx, &edit); err != nil {
|
||||
return nil, gtserror.Newf("error putting edit in database: %w", err)
|
||||
}
|
||||
|
||||
// Add edit to list of edits on the status.
|
||||
status.EditIDs = append(status.EditIDs, edit.ID)
|
||||
status.Edits = append(status.Edits, &edit)
|
||||
|
||||
// Add updated_at and edits to list of cols.
|
||||
cols = append(cols, "updated_at", "edits")
|
||||
}
|
||||
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
// getPopulatedMention tries to populate the given
|
||||
// mention with the correct TargetAccount and (if not
|
||||
// yet set) TargetAccountURI, returning the populated
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ func (d *Dereferencer) isPermittedStatus(
|
|||
requestUser string,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
isNew bool,
|
||||
) (
|
||||
permitted bool, // is permitted?
|
||||
err error,
|
||||
|
|
@ -98,7 +99,7 @@ func (d *Dereferencer) isPermittedStatus(
|
|||
permitted = true
|
||||
}
|
||||
|
||||
if !permitted && existing != nil {
|
||||
if !permitted && !isNew {
|
||||
log.Infof(ctx, "deleting unpermitted: %s", existing.URI)
|
||||
|
||||
// Delete existing status from database as it's no longer permitted.
|
||||
|
|
@ -110,11 +111,13 @@ func (d *Dereferencer) isPermittedStatus(
|
|||
return
|
||||
}
|
||||
|
||||
// isPermittedReply ...
|
||||
func (d *Dereferencer) isPermittedReply(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
reply *gtsmodel.Status,
|
||||
) (bool, error) {
|
||||
|
||||
var (
|
||||
replyURI = reply.URI // Definitely set.
|
||||
inReplyToURI = reply.InReplyToURI // Definitely set.
|
||||
|
|
@ -149,8 +152,7 @@ func (d *Dereferencer) isPermittedReply(
|
|||
// If this status's parent was rejected,
|
||||
// implicitly this reply should be too;
|
||||
// there's nothing more to check here.
|
||||
return false, d.unpermittedByParent(
|
||||
ctx,
|
||||
return false, d.unpermittedByParent(ctx,
|
||||
reply,
|
||||
thisReq,
|
||||
parentReq,
|
||||
|
|
@ -164,6 +166,7 @@ func (d *Dereferencer) isPermittedReply(
|
|||
// be approved, then we should just reject it
|
||||
// again, as nothing's changed since last time.
|
||||
if thisRejected && acceptIRI == "" {
|
||||
|
||||
// Nothing changed,
|
||||
// still rejected.
|
||||
return false, nil
|
||||
|
|
@ -174,6 +177,7 @@ func (d *Dereferencer) isPermittedReply(
|
|||
// to be approved. Continue permission checks.
|
||||
|
||||
if inReplyTo == nil {
|
||||
|
||||
// If we didn't have the replied-to status
|
||||
// in our database (yet), we can't check
|
||||
// right now if this reply is permitted.
|
||||
|
|
|
|||
|
|
@ -21,14 +21,21 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
// instantFreshness is the shortest possible freshness window.
|
||||
var instantFreshness = util.Ptr(dereferencing.FreshnessWindow(0))
|
||||
|
||||
type StatusTestSuite struct {
|
||||
DereferencerStandardTestSuite
|
||||
}
|
||||
|
|
@ -229,6 +236,219 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithNonMatchingURI() {
|
|||
suite.Nil(fetchedStatus)
|
||||
}
|
||||
|
||||
func (suite *StatusTestSuite) TestDereferencerRefreshStatusUpdated() {
|
||||
// Create a new context for this test.
|
||||
ctx, cncl := context.WithCancel(context.Background())
|
||||
defer cncl()
|
||||
|
||||
// The local account we will be fetching statuses as.
|
||||
fetchingAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
// The test status in question that we will be dereferencing from "remote".
|
||||
testURIStr := "https://unknown-instance.com/users/brand_new_person/statuses/01FE4NTHKWW7THT67EF10EB839"
|
||||
testURI := testrig.URLMustParse(testURIStr)
|
||||
testStatusable := suite.client.TestRemoteStatuses[testURIStr]
|
||||
|
||||
// Fetch the remote status first to load it into instance.
|
||||
testStatus, statusable, err := suite.dereferencer.GetStatusByURI(ctx,
|
||||
fetchingAccount.Username,
|
||||
testURI,
|
||||
)
|
||||
suite.NotNil(statusable)
|
||||
suite.NoError(err)
|
||||
|
||||
// Run through multiple possible edits.
|
||||
for _, testCase := range []struct {
|
||||
editedContent string
|
||||
editedContentWarning string
|
||||
editedLanguage string
|
||||
editedSensitive bool
|
||||
editedAttachmentIDs []string
|
||||
editedPollOptions []string
|
||||
editedPollVotes []int
|
||||
editedAt time.Time
|
||||
}{
|
||||
{
|
||||
editedContent: "updated status content!",
|
||||
editedContentWarning: "CW: edited status content",
|
||||
editedLanguage: testStatus.Language, // no change
|
||||
editedSensitive: *testStatus.Sensitive, // no change
|
||||
editedAttachmentIDs: testStatus.AttachmentIDs, // no change
|
||||
editedPollOptions: getPollOptions(testStatus), // no change
|
||||
editedPollVotes: getPollVotes(testStatus), // no change
|
||||
editedAt: time.Now(),
|
||||
},
|
||||
} {
|
||||
// Take a snapshot of current
|
||||
// state of the test status.
|
||||
testStatus = copyStatus(testStatus)
|
||||
|
||||
// Edit the "remote" statusable obj.
|
||||
suite.editStatusable(testStatusable,
|
||||
testCase.editedContent,
|
||||
testCase.editedContentWarning,
|
||||
testCase.editedLanguage,
|
||||
testCase.editedSensitive,
|
||||
testCase.editedAttachmentIDs,
|
||||
testCase.editedPollOptions,
|
||||
testCase.editedPollVotes,
|
||||
testCase.editedAt,
|
||||
)
|
||||
|
||||
// Refresh with a given statusable to updated to edited copy.
|
||||
latest, statusable, err := suite.dereferencer.RefreshStatus(ctx,
|
||||
fetchingAccount.Username,
|
||||
testStatus,
|
||||
nil, // NOTE: can provide testStatusable here to test as being received (not deref'd)
|
||||
instantFreshness,
|
||||
)
|
||||
suite.NotNil(statusable)
|
||||
suite.NoError(err)
|
||||
|
||||
// verify updated status details.
|
||||
suite.verifyEditedStatusUpdate(
|
||||
|
||||
// the original status
|
||||
// before any changes.
|
||||
testStatus,
|
||||
|
||||
// latest status
|
||||
// being tested.
|
||||
latest,
|
||||
|
||||
// expected current state.
|
||||
>smodel.StatusEdit{
|
||||
Content: testCase.editedContent,
|
||||
ContentWarning: testCase.editedContentWarning,
|
||||
Language: testCase.editedLanguage,
|
||||
Sensitive: &testCase.editedSensitive,
|
||||
AttachmentIDs: testCase.editedAttachmentIDs,
|
||||
PollOptions: testCase.editedPollOptions,
|
||||
PollVotes: testCase.editedPollVotes,
|
||||
// createdAt never changes
|
||||
},
|
||||
|
||||
// expected historic edit.
|
||||
>smodel.StatusEdit{
|
||||
Content: testStatus.Content,
|
||||
ContentWarning: testStatus.ContentWarning,
|
||||
Language: testStatus.Language,
|
||||
Sensitive: testStatus.Sensitive,
|
||||
AttachmentIDs: testStatus.AttachmentIDs,
|
||||
PollOptions: getPollOptions(testStatus),
|
||||
PollVotes: getPollVotes(testStatus),
|
||||
CreatedAt: testStatus.UpdatedAt,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// editStatusable updates the given statusable attributes.
|
||||
// note that this acts on the original object, no copying.
|
||||
func (suite *StatusTestSuite) editStatusable(
|
||||
statusable ap.Statusable,
|
||||
content string,
|
||||
contentWarning string,
|
||||
language string,
|
||||
sensitive bool,
|
||||
attachmentIDs []string, // TODO: this will require some thinking as to how ...
|
||||
pollOptions []string, // TODO: this will require changing statusable type to question
|
||||
pollVotes []int, // TODO: this will require changing statusable type to question
|
||||
editedAt time.Time,
|
||||
) {
|
||||
// simply reset all mentions / emojis / tags
|
||||
statusable.SetActivityStreamsTag(nil)
|
||||
|
||||
// Update the statusable content property + language (if set).
|
||||
contentProp := streams.NewActivityStreamsContentProperty()
|
||||
statusable.SetActivityStreamsContent(contentProp)
|
||||
contentProp.AppendXMLSchemaString(content)
|
||||
if language != "" {
|
||||
contentProp.AppendRDFLangString(map[string]string{
|
||||
language: content,
|
||||
})
|
||||
}
|
||||
|
||||
// Update the statusable content-warning property.
|
||||
summaryProp := streams.NewActivityStreamsSummaryProperty()
|
||||
statusable.SetActivityStreamsSummary(summaryProp)
|
||||
summaryProp.AppendXMLSchemaString(contentWarning)
|
||||
|
||||
// Update the statusable sensitive property.
|
||||
sensitiveProp := streams.NewActivityStreamsSensitiveProperty()
|
||||
statusable.SetActivityStreamsSensitive(sensitiveProp)
|
||||
sensitiveProp.AppendXMLSchemaBoolean(sensitive)
|
||||
|
||||
// Update the statusable updated property.
|
||||
ap.SetUpdated(statusable, editedAt)
|
||||
}
|
||||
|
||||
// verifyEditedStatusUpdate verifies that a given status has
|
||||
// the expected number of historic edits, the 'current' status
|
||||
// attributes (encapsulated as an edit for minimized no. args),
|
||||
// and the last given 'historic' status edit attributes.
|
||||
func (suite *StatusTestSuite) verifyEditedStatusUpdate(
|
||||
testStatus *gtsmodel.Status, // the original model
|
||||
status *gtsmodel.Status, // the status to check
|
||||
current *gtsmodel.StatusEdit, // expected current state
|
||||
historic *gtsmodel.StatusEdit, // historic edit we expect to have
|
||||
) {
|
||||
// don't use this func
|
||||
// name in error msgs.
|
||||
suite.T().Helper()
|
||||
|
||||
// Check we have expected number of edits.
|
||||
previousEdits := len(testStatus.Edits)
|
||||
suite.Len(status.Edits, previousEdits+1)
|
||||
suite.Len(status.EditIDs, previousEdits+1)
|
||||
|
||||
// Check current state of status.
|
||||
suite.Equal(current.Content, status.Content)
|
||||
suite.Equal(current.ContentWarning, status.ContentWarning)
|
||||
suite.Equal(current.Language, status.Language)
|
||||
suite.Equal(*current.Sensitive, *status.Sensitive)
|
||||
suite.Equal(current.AttachmentIDs, status.AttachmentIDs)
|
||||
suite.Equal(current.PollOptions, getPollOptions(status))
|
||||
suite.Equal(current.PollVotes, getPollVotes(status))
|
||||
|
||||
// Check the latest historic edit matches expected.
|
||||
latestEdit := status.Edits[len(status.Edits)-1]
|
||||
suite.Equal(historic.Content, latestEdit.Content)
|
||||
suite.Equal(historic.ContentWarning, latestEdit.ContentWarning)
|
||||
suite.Equal(historic.Language, latestEdit.Language)
|
||||
suite.Equal(*historic.Sensitive, *latestEdit.Sensitive)
|
||||
suite.Equal(historic.AttachmentIDs, latestEdit.AttachmentIDs)
|
||||
suite.Equal(historic.PollOptions, latestEdit.PollOptions)
|
||||
suite.Equal(historic.PollVotes, latestEdit.PollVotes)
|
||||
suite.Equal(historic.CreatedAt, latestEdit.CreatedAt)
|
||||
|
||||
// The status creation date should never change.
|
||||
suite.Equal(testStatus.CreatedAt, status.CreatedAt)
|
||||
}
|
||||
|
||||
func TestStatusTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusTestSuite))
|
||||
}
|
||||
|
||||
// copyStatus returns a copy of the given status model (not including sub-structs).
|
||||
func copyStatus(status *gtsmodel.Status) *gtsmodel.Status {
|
||||
copy := new(gtsmodel.Status)
|
||||
*copy = *status
|
||||
return copy
|
||||
}
|
||||
|
||||
// getPollOptions extracts poll option strings from status (if poll is set).
|
||||
func getPollOptions(status *gtsmodel.Status) []string {
|
||||
if status.Poll != nil {
|
||||
return status.Poll.Options
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getPollVotes extracts poll vote counts from status (if poll is set).
|
||||
func getPollVotes(status *gtsmodel.Status) []int {
|
||||
if status.Poll != nil {
|
||||
return status.Poll.Votes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,15 +52,15 @@ func emojiChanged(existing, latest *gtsmodel.Emoji) bool {
|
|||
|
||||
// pollChanged returns whether a poll has changed in way that
|
||||
// indicates that this should be an entirely new poll. i.e. if
|
||||
// the available options have changed, or the expiry has increased.
|
||||
// the available options have changed, or the expiry has changed.
|
||||
func pollChanged(existing, latest *gtsmodel.Poll) bool {
|
||||
return !slices.Equal(existing.Options, latest.Options) ||
|
||||
!existing.ExpiresAt.Equal(latest.ExpiresAt)
|
||||
}
|
||||
|
||||
// pollUpdated returns whether a poll has updated, i.e. if the
|
||||
// pollStateUpdated returns whether a poll has updated, i.e. if
|
||||
// vote counts have changed, or if it has expired / been closed.
|
||||
func pollUpdated(existing, latest *gtsmodel.Poll) bool {
|
||||
func pollStateUpdated(existing, latest *gtsmodel.Poll) bool {
|
||||
return *existing.Voters != *latest.Voters ||
|
||||
!slices.Equal(existing.Votes, latest.Votes) ||
|
||||
!existing.ClosedAt.Equal(latest.ClosedAt)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue