diff options
Diffstat (limited to 'internal/typeutils')
-rw-r--r-- | internal/typeutils/astointernal.go | 6 | ||||
-rw-r--r-- | internal/typeutils/internaltoas.go | 158 | ||||
-rw-r--r-- | internal/typeutils/internaltoas_test.go | 4 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend.go | 101 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend_test.go | 50 | ||||
-rw-r--r-- | internal/typeutils/wrap.go | 175 | ||||
-rw-r--r-- | internal/typeutils/wrap_test.go | 9 |
7 files changed, 349 insertions, 154 deletions
diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index 92465c790..707f51629 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -261,8 +261,10 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // Attached poll information (the statusable will actually // be a Pollable, as a Question is a subset of our Status). if pollable, ok := ap.ToPollable(statusable); ok { - // TODO: handle decoding poll data - _ = pollable + status.Poll, err = ap.ExtractPoll(pollable) + if err != nil { + l.Warnf("error(s) extracting poll: %v", err) + } } // status.Hashtags diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index b920d9a0e..a668989e6 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -412,8 +412,24 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat return nil, gtserror.Newf("error populating status: %w", err) } - // We convert it as an AS Note. - status := streams.NewActivityStreamsNote() + var status ap.Statusable + + if s.Poll != nil { + // If status has poll available, we convert + // it as an AS Question (similar to a Note). + poll := streams.NewActivityStreamsQuestion() + + // Add required status poll data to AS Question. + if err := c.addPollToAS(ctx, s.Poll, poll); err != nil { + return nil, gtserror.Newf("error converting poll: %w", err) + } + + // Set poll as status. + status = poll + } else { + // Else we converter it as an AS Note. + status = streams.NewActivityStreamsNote() + } // id statusURI, err := url.Parse(s.URI) @@ -636,6 +652,73 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat return status, nil } +func (c *Converter) addPollToAS(ctx context.Context, poll *gtsmodel.Poll, dst ap.Pollable) error { + var optionsProp interface { + // the minimum interface for appending AS Notes + // to an AS type options property of some kind. + AppendActivityStreamsNote(vocab.ActivityStreamsNote) + } + + if len(poll.Options) != len(poll.Votes) { + return gtserror.Newf("invalid poll %s", poll.ID) + } + + if !*poll.HideCounts { + // Set total no. voting accounts. + ap.SetVotersCount(dst, *poll.Voters) + } + + if *poll.Multiple { + // Create new multiple-choice (AnyOf) property for poll. + anyOfProp := streams.NewActivityStreamsAnyOfProperty() + dst.SetActivityStreamsAnyOf(anyOfProp) + optionsProp = anyOfProp + } else { + // Create new single-choice (OneOf) property for poll. + oneOfProp := streams.NewActivityStreamsOneOfProperty() + dst.SetActivityStreamsOneOf(oneOfProp) + optionsProp = oneOfProp + } + + for i, name := range poll.Options { + // Create new Note object to represent option. + note := streams.NewActivityStreamsNote() + + // Create new name property and set the option name. + nameProp := streams.NewActivityStreamsNameProperty() + nameProp.AppendXMLSchemaString(name) + note.SetActivityStreamsName(nameProp) + + if !*poll.HideCounts { + // Create new total items property to hold the vote count. + totalItemsProp := streams.NewActivityStreamsTotalItemsProperty() + totalItemsProp.Set(poll.Votes[i]) + + // Create new replies property with collection to encompass count. + repliesProp := streams.NewActivityStreamsRepliesProperty() + collection := streams.NewActivityStreamsCollection() + collection.SetActivityStreamsTotalItems(totalItemsProp) + repliesProp.SetActivityStreamsCollection(collection) + + // Attach the replies to Note object. + note.SetActivityStreamsReplies(repliesProp) + } + + // Append the note to options property. + optionsProp.AppendActivityStreamsNote(note) + } + + // Set poll endTime property. + ap.SetEndTime(dst, poll.ExpiresAt) + + if !poll.ClosedAt.IsZero() { + // Poll is closed, set closed property. + ap.AppendClosed(dst, poll.ClosedAt) + } + + return nil +} + // StatusToASDelete converts a gts model status into a Delete of that status, using just the // URI of the status as object, and addressing the Delete appropriately. func (c *Converter) StatusToASDelete(ctx context.Context, s *gtsmodel.Status) (vocab.ActivityStreamsDelete, error) { @@ -1413,12 +1496,8 @@ func (c *Converter) StatusesToASOutboxPage(ctx context.Context, outboxID string, return nil, err } - create, err := c.WrapStatusableInCreate(note, true) - if err != nil { - return nil, err - } - - itemsProp.AppendActivityStreamsCreate(create) + activity := WrapStatusableInCreate(note, true) + itemsProp.AppendActivityStreamsCreate(activity) if highest == "" || s.ID > highest { highest = s.ID @@ -1569,3 +1648,66 @@ func (c *Converter) ReportToASFlag(ctx context.Context, r *gtsmodel.Report) (voc return flag, nil } + +func (c *Converter) PollVoteToASOptions(ctx context.Context, vote *gtsmodel.PollVote) ([]ap.PollOptionable, error) { + // Ensure the vote is fully populated (this fetches author). + if err := c.state.DB.PopulatePollVote(ctx, vote); err != nil { + return nil, gtserror.Newf("error populating vote from db: %w", err) + } + + // Get the vote author. + author := vote.Account + + // Get the JSONLD ID IRI for vote author. + authorIRI, err := url.Parse(author.URI) + if err != nil { + return nil, gtserror.Newf("invalid author uri: %w", err) + } + + // Get the vote poll. + poll := vote.Poll + + // Ensure the poll is fully populated with status. + if err := c.state.DB.PopulatePoll(ctx, poll); err != nil { + return nil, gtserror.Newf("error populating poll from db: %w", err) + } + + // Get the JSONLD ID IRI for poll's source status. + statusIRI, err := url.Parse(poll.Status.URI) + if err != nil { + return nil, gtserror.Newf("invalid status uri: %w", err) + } + + // Get the JSONLD ID IRI for poll's author account. + pollAuthorIRI, err := url.Parse(poll.Status.AccountURI) + if err != nil { + return nil, gtserror.Newf("invalid account uri: %w", err) + } + + // Preallocate the return slice of notes. + notes := make([]ap.PollOptionable, len(vote.Choices)) + + for i, choice := range vote.Choices { + // Create new note to represent vote. + note := streams.NewActivityStreamsNote() + + // For AP IRI generate from author URI + poll ID + vote choice. + id := fmt.Sprintf("%s#%s/votes/%d", author.URI, poll.ID, choice) + ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(note), id) + + // Attach new name property to note with vote choice. + nameProp := streams.NewActivityStreamsNameProperty() + nameProp.AppendXMLSchemaString(poll.Options[choice]) + note.SetActivityStreamsName(nameProp) + + // Set 'to', 'attribTo', 'inReplyTo' fields. + ap.AppendAttributedTo(note, authorIRI) + ap.AppendInReplyTo(note, statusIRI) + ap.AppendTo(note, pollAuthorIRI) + + // Set note in return slice. + notes[i] = note + } + + return notes, nil +} diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go index 30e4f2135..01dde66fb 100644 --- a/internal/typeutils/internaltoas_test.go +++ b/internal/typeutils/internaltoas_test.go @@ -680,7 +680,7 @@ func (suite *InternalToASTestSuite) TestStatusesToASOutboxPage() { { "actor": "http://localhost:8080/users/admin", "cc": "http://localhost:8080/users/admin/followers", - "id": "http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37/activity", + "id": "http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37/activity#Create", "object": "http://localhost:8080/users/admin/statuses/01F8MHAAY43M6RJ473VQFCVH37", "published": "2021-10-20T12:36:45Z", "to": "https://www.w3.org/ns/activitystreams#Public", @@ -689,7 +689,7 @@ func (suite *InternalToASTestSuite) TestStatusesToASOutboxPage() { { "actor": "http://localhost:8080/users/admin", "cc": "http://localhost:8080/users/admin/followers", - "id": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/activity", + "id": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/activity#Create", "object": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", "published": "2021-10-20T11:36:45Z", "to": "https://www.w3.org/ns/activitystreams#Public", diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 254bf9da3..6a374bbde 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -729,9 +729,12 @@ func (c *Converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r } if appID := s.CreatedWithApplicationID; appID != "" { - app, err := c.state.DB.GetApplicationByID(ctx, appID) - if err != nil { - return nil, fmt.Errorf("error getting application %s: %w", appID, err) + app := s.CreatedWithApplication + if app == nil { + app, err = c.state.DB.GetApplicationByID(ctx, appID) + if err != nil { + return nil, fmt.Errorf("error getting application %s: %w", appID, err) + } } apiApp, err := c.AppToAPIAppPublic(ctx, app) @@ -742,6 +745,18 @@ func (c *Converter) StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, r apiStatus.Application = apiApp } + if s.Poll != nil { + // Set originating + // status on the poll. + poll := s.Poll + poll.Status = s + + apiStatus.Poll, err = c.PollToAPIPoll(ctx, requestingAccount, poll) + if err != nil { + return nil, fmt.Errorf("error converting poll: %w", err) + } + } + // Normalization. if s.URL == "" { @@ -1287,6 +1302,86 @@ func (c *Converter) MarkersToAPIMarker(ctx context.Context, markers []*gtsmodel. return apiMarker, nil } +// PollToAPIPoll converts a database (gtsmodel) Poll into an API model representation appropriate for the given requesting account. +func (c *Converter) PollToAPIPoll(ctx context.Context, requester *gtsmodel.Account, poll *gtsmodel.Poll) (*apimodel.Poll, error) { + // Ensure the poll model is fully populated for src status. + if err := c.state.DB.PopulatePoll(ctx, poll); err != nil { + return nil, gtserror.Newf("error populating poll: %w", err) + } + + var ( + totalVotes int + totalVoters int + voteCounts []int + ownChoices []int + isAuthor bool + ) + + if requester != nil { + // Get vote by requester in poll (if any). + vote, err := c.state.DB.GetPollVoteBy(ctx, + poll.ID, + requester.ID, + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.Newf("error getting vote for poll %s: %w", poll.ID, err) + } + + if vote != nil { + // Set choices by requester. + ownChoices = vote.Choices + + // Update default totals in the + // case that counts are hidden. + totalVotes = len(vote.Choices) + totalVoters = 1 + } + + // Check if requester is author of source status. + isAuthor = (requester.ID == poll.Status.AccountID) + } + + // Preallocate a slice of frontend model poll choices. + options := make([]apimodel.PollOption, len(poll.Options)) + + // Add the titles to all of the options. + for i, title := range poll.Options { + options[i].Title = title + } + + if isAuthor || !*poll.HideCounts { + // A remote status, + // the simple route! + // + // Pull cached remote values. + totalVoters = *poll.Voters + voteCounts = poll.Votes + + // Accumulate total from all counts. + for _, count := range poll.Votes { + totalVotes += count + } + + // When this is status author, or hide counts + // is disabled, set the counts known per vote. + for i, count := range voteCounts { + options[i].VotesCount = count + } + } + + return &apimodel.Poll{ + ID: poll.ID, + ExpiresAt: util.FormatISO8601(poll.ExpiresAt), + Expired: poll.Closed(), + Multiple: *poll.Multiple, + VotesCount: totalVotes, + VotersCount: totalVoters, + Voted: (isAuthor || len(ownChoices) > 0), + OwnVotes: ownChoices, + Options: options, + }, nil +} + // convertAttachmentsToAPIAttachments will convert a slice of GTS model attachments to frontend API model attachments, falling back to IDs if no GTS models supplied. func (c *Converter) convertAttachmentsToAPIAttachments(ctx context.Context, attachments []*gtsmodel.MediaAttachment, attachmentIDs []string) ([]apimodel.Attachment, error) { var errs gtserror.MultiError diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 16966d8cb..0e09faeea 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -58,8 +58,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() { "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "followers_count": 2, "following_count": 2, - "statuses_count": 5, - "last_status_at": "2022-05-20T11:37:55.000Z", + "statuses_count": 6, + "last_status_at": "2022-05-20T11:41:10.000Z", "emojis": [], "fields": [], "enable_rss": true, @@ -100,8 +100,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct() "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "followers_count": 2, "following_count": 2, - "statuses_count": 5, - "last_status_at": "2022-05-20T11:37:55.000Z", + "statuses_count": 6, + "last_status_at": "2022-05-20T11:41:10.000Z", "emojis": [ { "shortcode": "rainbow", @@ -148,8 +148,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiIDs() { "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "followers_count": 2, "following_count": 2, - "statuses_count": 5, - "last_status_at": "2022-05-20T11:37:55.000Z", + "statuses_count": 6, + "last_status_at": "2022-05-20T11:41:10.000Z", "emojis": [ { "shortcode": "rainbow", @@ -192,8 +192,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() { "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "followers_count": 2, "following_count": 2, - "statuses_count": 5, - "last_status_at": "2022-05-20T11:37:55.000Z", + "statuses_count": 6, + "last_status_at": "2022-05-20T11:41:10.000Z", "emojis": [], "fields": [], "source": { @@ -660,7 +660,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() { }, "stats": { "domain_count": 2, - "status_count": 16, + "status_count": 18, "user_count": 4 }, "thumbnail": "http://localhost:8080/assets/logo.png", @@ -910,8 +910,8 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend1() { "header_static": "http://localhost:8080/assets/default_header.png", "followers_count": 0, "following_count": 0, - "statuses_count": 1, - "last_status_at": "2021-09-20T10:40:37.000Z", + "statuses_count": 2, + "last_status_at": "2021-09-11T09:40:37.000Z", "emojis": [], "fields": [] } @@ -953,8 +953,8 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend2() { "header_static": "http://localhost:8080/assets/default_header.png", "followers_count": 1, "following_count": 1, - "statuses_count": 7, - "last_status_at": "2021-10-20T10:40:37.000Z", + "statuses_count": 8, + "last_status_at": "2021-07-28T08:40:37.000Z", "emojis": [], "fields": [ { @@ -1027,8 +1027,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() { "header_static": "http://localhost:8080/assets/default_header.png", "followers_count": 0, "following_count": 0, - "statuses_count": 1, - "last_status_at": "2021-09-20T10:40:37.000Z", + "statuses_count": 2, + "last_status_at": "2021-09-11T09:40:37.000Z", "emojis": [], "fields": [] } @@ -1068,8 +1068,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() { "header_static": "http://localhost:8080/assets/default_header.png", "followers_count": 1, "following_count": 1, - "statuses_count": 7, - "last_status_at": "2021-10-20T10:40:37.000Z", + "statuses_count": 8, + "last_status_at": "2021-07-28T08:40:37.000Z", "emojis": [], "fields": [ { @@ -1239,8 +1239,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() { "header_static": "http://localhost:8080/assets/default_header.png", "followers_count": 1, "following_count": 1, - "statuses_count": 7, - "last_status_at": "2021-10-20T10:40:37.000Z", + "statuses_count": 8, + "last_status_at": "2021-07-28T08:40:37.000Z", "emojis": [], "fields": [ { @@ -1295,8 +1295,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() { "header_static": "http://localhost:8080/assets/default_header.png", "followers_count": 0, "following_count": 0, - "statuses_count": 1, - "last_status_at": "2021-09-20T10:40:37.000Z", + "statuses_count": 2, + "last_status_at": "2021-09-11T09:40:37.000Z", "emojis": [], "fields": [] } @@ -1342,8 +1342,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() { "header_static": "http://localhost:8080/assets/default_header.png", "followers_count": 0, "following_count": 0, - "statuses_count": 1, - "last_status_at": "2021-09-20T10:40:37.000Z", + "statuses_count": 2, + "last_status_at": "2021-09-11T09:40:37.000Z", "emojis": [], "fields": [] }, @@ -1473,8 +1473,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca "header_static": "http://localhost:8080/assets/default_header.png", "followers_count": 0, "following_count": 0, - "statuses_count": 1, - "last_status_at": "2021-09-20T10:40:37.000Z", + "statuses_count": 2, + "last_status_at": "2021-09-11T09:40:37.000Z", "emojis": [], "fields": [] } diff --git a/internal/typeutils/wrap.go b/internal/typeutils/wrap.go index 128c4ef15..5deca0e5b 100644 --- a/internal/typeutils/wrap.go +++ b/internal/typeutils/wrap.go @@ -19,6 +19,7 @@ package typeutils import ( "net/url" + "time" "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" @@ -84,132 +85,86 @@ func (c *Converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi return update, nil } -// WrapNoteInCreate wraps a Statusable with a Create activity. -// -// If objectIRIOnly is set to true, then the function won't put the *entire* note in the Object field of the Create, -// but just the AP URI of the note. This is useful in cases where you want to give a remote server something to dereference, -// and still have control over whether or not they're allowed to actually see the contents. -func (c *Converter) WrapStatusableInCreate(status ap.Statusable, objectIRIOnly bool) (vocab.ActivityStreamsCreate, error) { +func WrapStatusableInCreate(status ap.Statusable, iriOnly bool) vocab.ActivityStreamsCreate { create := streams.NewActivityStreamsCreate() + wrapStatusableInActivity(create, status, iriOnly) + return create +} - // Object property - objectProp := streams.NewActivityStreamsObjectProperty() - if objectIRIOnly { - // Only append the object IRI to objectProp. - objectProp.AppendIRI(status.GetJSONLDId().GetIRI()) - } else { - // Our statusable's are always note types. - asNote := status.(vocab.ActivityStreamsNote) - objectProp.AppendActivityStreamsNote(asNote) +func WrapPollOptionablesInCreate(options ...ap.PollOptionable) vocab.ActivityStreamsCreate { + if len(options) == 0 { + panic("no options") } - create.SetActivityStreamsObject(objectProp) - // ID property - idProp := streams.NewJSONLDIdProperty() - createID := status.GetJSONLDId().GetIRI().String() + "/activity" - createIDIRI, err := url.Parse(createID) - if err != nil { - return nil, err + // Extract attributedTo IRI from any option. + attribTos := ap.GetAttributedTo(options[0]) + if len(attribTos) != 1 { + panic("invalid attributedTo count") } - idProp.SetIRI(createIDIRI) - create.SetJSONLDId(idProp) - // Actor Property - actorProp := streams.NewActivityStreamsActorProperty() - actorIRI, err := ap.ExtractAttributedToURI(status) - if err != nil { - return nil, gtserror.Newf("couldn't extract AttributedTo: %w", err) + // Extract target status IRI from any option. + replyTos := ap.GetInReplyTo(options[0]) + if len(replyTos) != 1 { + panic("invalid inReplyTo count") } - actorProp.AppendIRI(actorIRI) - create.SetActivityStreamsActor(actorProp) - // Published Property - publishedProp := streams.NewActivityStreamsPublishedProperty() - published, err := ap.ExtractPublished(status) - if err != nil { - return nil, gtserror.Newf("couldn't extract Published: %w", err) - } - publishedProp.Set(published) - create.SetActivityStreamsPublished(publishedProp) + // Allocate create activity and copy over 'To' property. + create := streams.NewActivityStreamsCreate() + ap.AppendTo(create, ap.GetTo(options[0])...) - // To Property - toProp := streams.NewActivityStreamsToProperty() - if toURIs := ap.ExtractToURIs(status); len(toURIs) != 0 { - for _, toURI := range toURIs { - toProp.AppendIRI(toURI) - } - create.SetActivityStreamsTo(toProp) - } + // Activity ID formatted as: {$statusIRI}/activity#vote/{$voterIRI}. + id := replyTos[0].String() + "/activity#vote/" + attribTos[0].String() + ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(create), id) + + // Set a current publish time for activity. + ap.SetPublished(create, time.Now()) - // Cc Property - ccProp := streams.NewActivityStreamsCcProperty() - if ccURIs := ap.ExtractCcURIs(status); len(ccURIs) != 0 { - for _, ccURI := range ccURIs { - ccProp.AppendIRI(ccURI) - } - create.SetActivityStreamsCc(ccProp) + // Append each poll option as object to activity. + for _, option := range options { + status, _ := ap.ToStatusable(option) + appendStatusableToActivity(create, status, false) } - return create, nil + return create } -// WrapStatusableInUpdate wraps a Statusable with an Update activity. -// -// If objectIRIOnly is set to true, then the function won't put the *entire* note in the Object field of the Create, -// but just the AP URI of the note. This is useful in cases where you want to give a remote server something to dereference, -// and still have control over whether or not they're allowed to actually see the contents. -func (c *Converter) WrapStatusableInUpdate(status ap.Statusable, objectIRIOnly bool) (vocab.ActivityStreamsUpdate, error) { +func WrapStatusableInUpdate(status ap.Statusable, iriOnly bool) vocab.ActivityStreamsUpdate { update := streams.NewActivityStreamsUpdate() + wrapStatusableInActivity(update, status, iriOnly) + return update +} - // Object property - objectProp := streams.NewActivityStreamsObjectProperty() - if objectIRIOnly { - objectProp.AppendIRI(status.GetJSONLDId().GetIRI()) - } else if _, ok := status.(ap.Pollable); ok { - asQuestion := status.(vocab.ActivityStreamsQuestion) - objectProp.AppendActivityStreamsQuestion(asQuestion) - } else { - asNote := status.(vocab.ActivityStreamsNote) - objectProp.AppendActivityStreamsNote(asNote) - } - update.SetActivityStreamsObject(objectProp) - - // ID property - idProp := streams.NewJSONLDIdProperty() - createID := status.GetJSONLDId().GetIRI().String() + "/activity" - createIDIRI, err := url.Parse(createID) - if err != nil { - return nil, err - } - idProp.SetIRI(createIDIRI) - update.SetJSONLDId(idProp) - - // Actor Property - actorProp := streams.NewActivityStreamsActorProperty() - actorIRI, err := ap.ExtractAttributedToURI(status) - if err != nil { - return nil, gtserror.Newf("couldn't extract AttributedTo: %w", err) - } - actorProp.AppendIRI(actorIRI) - update.SetActivityStreamsActor(actorProp) - - // To Property - toProp := streams.NewActivityStreamsToProperty() - if toURIs := ap.ExtractToURIs(status); len(toURIs) != 0 { - for _, toURI := range toURIs { - toProp.AppendIRI(toURI) - } - update.SetActivityStreamsTo(toProp) - } +// wrapStatusableInActivity adds the required ap.Statusable data to the given ap.Activityable. +func wrapStatusableInActivity(activity ap.Activityable, status ap.Statusable, iriOnly bool) { + idIRI := ap.GetJSONLDId(status) // activity ID formatted as {$statusIRI}/activity#{$typeName} + ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(activity), idIRI.String()+"/activity#"+activity.GetTypeName()) + appendStatusableToActivity(activity, status, iriOnly) + ap.AppendTo(activity, ap.GetTo(status)...) + ap.AppendCc(activity, ap.GetCc(status)...) + ap.AppendActor(activity, ap.GetAttributedTo(status)...) + ap.SetPublished(activity, ap.GetPublished(status)) +} - // Cc Property - ccProp := streams.NewActivityStreamsCcProperty() - if ccURIs := ap.ExtractCcURIs(status); len(ccURIs) != 0 { - for _, ccURI := range ccURIs { - ccProp.AppendIRI(ccURI) - } - update.SetActivityStreamsCc(ccProp) +// appendStatusableToActivity appends a Statusable type to an Activityable, handling case of Question, Note or just IRI type. +func appendStatusableToActivity(activity ap.Activityable, status ap.Statusable, iriOnly bool) { + // Get existing object property or allocate new. + objProp := activity.GetActivityStreamsObject() + if objProp == nil { + objProp = streams.NewActivityStreamsObjectProperty() + activity.SetActivityStreamsObject(objProp) + } + + if iriOnly { + // Only append status IRI. + idIRI := ap.GetJSONLDId(status) + objProp.AppendIRI(idIRI) + } else if poll, ok := ap.ToPollable(status); ok { + // Our Pollable implementer is an AS Question type. + question := poll.(vocab.ActivityStreamsQuestion) + objProp.AppendActivityStreamsQuestion(question) + } else { + // All of our other Statusable types are AS Note. + note := status.(vocab.ActivityStreamsNote) + objProp.AppendActivityStreamsNote(note) } - - return update, nil } diff --git a/internal/typeutils/wrap_test.go b/internal/typeutils/wrap_test.go index 51f67f455..9d6d95983 100644 --- a/internal/typeutils/wrap_test.go +++ b/internal/typeutils/wrap_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" ) type WrapTestSuite struct { @@ -36,7 +37,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreateIRIOnly() { note, err := suite.typeconverter.StatusToAS(context.Background(), testStatus) suite.NoError(err) - create, err := suite.typeconverter.WrapStatusableInCreate(note, true) + create := typeutils.WrapStatusableInCreate(note, true) suite.NoError(err) suite.NotNil(create) @@ -50,7 +51,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreateIRIOnly() { "@context": "https://www.w3.org/ns/activitystreams", "actor": "http://localhost:8080/users/the_mighty_zork", "cc": "http://localhost:8080/users/the_mighty_zork/followers", - "id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity", + "id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity#Create", "object": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY", "published": "2021-10-20T12:40:37+02:00", "to": "https://www.w3.org/ns/activitystreams#Public", @@ -64,7 +65,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() { note, err := suite.typeconverter.StatusToAS(context.Background(), testStatus) suite.NoError(err) - create, err := suite.typeconverter.WrapStatusableInCreate(note, false) + create := typeutils.WrapStatusableInCreate(note, false) suite.NoError(err) suite.NotNil(create) @@ -78,7 +79,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() { "@context": "https://www.w3.org/ns/activitystreams", "actor": "http://localhost:8080/users/the_mighty_zork", "cc": "http://localhost:8080/users/the_mighty_zork/followers", - "id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity", + "id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity#Create", "object": { "attachment": [], "attributedTo": "http://localhost:8080/users/the_mighty_zork", |