[feature] add support for polls + receiving federated status edits (#2330)

This commit is contained in:
kim
2023-11-08 14:32:17 +00:00
committed by GitHub
parent 7204ccedc3
commit e9e5dc5a40
84 changed files with 3992 additions and 570 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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

View File

@@ -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": []
}

View File

@@ -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()
// 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)
}
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
}
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)
}
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)
// To Property
toProp := streams.NewActivityStreamsToProperty()
if toURIs := ap.ExtractToURIs(status); len(toURIs) != 0 {
for _, toURI := range toURIs {
toProp.AppendIRI(toURI)
}
create.SetActivityStreamsTo(toProp)
}
// Cc Property
ccProp := streams.NewActivityStreamsCcProperty()
if ccURIs := ap.ExtractCcURIs(status); len(ccURIs) != 0 {
for _, ccURI := range ccURIs {
ccProp.AppendIRI(ccURI)
}
create.SetActivityStreamsCc(ccProp)
}
return create, nil
wrapStatusableInActivity(create, status, iriOnly)
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 WrapPollOptionablesInCreate(options ...ap.PollOptionable) vocab.ActivityStreamsCreate {
if len(options) == 0 {
panic("no options")
}
// Extract attributedTo IRI from any option.
attribTos := ap.GetAttributedTo(options[0])
if len(attribTos) != 1 {
panic("invalid attributedTo count")
}
// Extract target status IRI from any option.
replyTos := ap.GetInReplyTo(options[0])
if len(replyTos) != 1 {
panic("invalid inReplyTo count")
}
// Allocate create activity and copy over 'To' property.
create := streams.NewActivityStreamsCreate()
ap.AppendTo(create, ap.GetTo(options[0])...)
// 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())
// Append each poll option as object to activity.
for _, option := range options {
status, _ := ap.ToStatusable(option)
appendStatusableToActivity(create, status, false)
}
return create
}
func WrapStatusableInUpdate(status ap.Statusable, iriOnly bool) vocab.ActivityStreamsUpdate {
update := streams.NewActivityStreamsUpdate()
// 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)
}
// Cc Property
ccProp := streams.NewActivityStreamsCcProperty()
if ccURIs := ap.ExtractCcURIs(status); len(ccURIs) != 0 {
for _, ccURI := range ccURIs {
ccProp.AppendIRI(ccURI)
}
update.SetActivityStreamsCc(ccProp)
}
return update, nil
wrapStatusableInActivity(update, status, iriOnly)
return update
}
// 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))
}
// 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)
}
}

View File

@@ -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",