mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-22 11:46:40 +00:00
[bugfix] Add Actor to outgoing poll vote Create; other fixes (#2384)
This commit is contained in:
parent
5c17ecd93a
commit
e4e0a5e3f6
|
@ -656,10 +656,41 @@ For example:
|
||||||
|
|
||||||
### Outgoing
|
### Outgoing
|
||||||
|
|
||||||
You can expect to receive poll votes from GoToSocial in the form of "Note" objects, as specifically described in the section above. These will only ever be sent out as the object attached to a "Create" activity.
|
You can expect to receive poll votes from GoToSocial in the form of "Note" objects, as specifically described in the section above. These will only ever be sent out as the object(s) attached to a "Create" activity.
|
||||||
|
|
||||||
In particular, as described in the section above, GoToSocial will provide the option text in the "name" field, the "content" field unset, and the "inReplyTo" field being an IRI pointing toward a status with poll authored on your instance.
|
In particular, as described in the section above, GoToSocial will provide the option text in the "name" field, the "content" field unset, and the "inReplyTo" field being an IRI pointing toward a status with poll authored on your instance.
|
||||||
|
|
||||||
|
Here's an example of a "Create", in which user "https://sample.com/users/willy_nilly" votes on a multiple-choice poll created by user "https://example.org/users/bobby_tables":
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"actor": "https://sample.com/users/willy_nilly",
|
||||||
|
"id": "https://sample.com/users/willy_nilly/activity#vote/https://example.org/users/bobby_tables/statuses/123456",
|
||||||
|
"object": [
|
||||||
|
{
|
||||||
|
"attributedTo": "https://sample.com/users/willy_nilly",
|
||||||
|
"id": "https://sample.com/users/willy_nilly#01HEN2R65468ZG657C4ZPHJ4EX/votes/1",
|
||||||
|
"inReplyTo": "https://example.org/users/bobby_tables/statuses/123456",
|
||||||
|
"name": "tissues",
|
||||||
|
"to": "https://example.org/users/bobby_tables",
|
||||||
|
"type": "Note"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributedTo": "https://sample.com/users/willy_nilly",
|
||||||
|
"id": "https://sample.com/users/willy_nilly#01HEN2R65468ZG657C4ZPHJ4EX/votes/2",
|
||||||
|
"inReplyTo": "https://example.org/users/bobby_tables/statuses/123456",
|
||||||
|
"name": "financial times",
|
||||||
|
"to": "https://example.org/users/bobby_tables",
|
||||||
|
"type": "Note"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"published": "2021-09-11T11:45:37+02:00",
|
||||||
|
"to": "https://example.org/users/bobby_tables",
|
||||||
|
"type": "Create"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Incoming
|
### Incoming
|
||||||
|
|
||||||
GoToSocial expects to receive poll votes in much the same manner that it sends them out. They will only ever expect to be received as part of a "Create" activity.
|
GoToSocial expects to receive poll votes in much the same manner that it sends them out. They will only ever expect to be received as part of a "Create" activity.
|
||||||
|
|
|
@ -194,14 +194,13 @@ func (f *federate) CreatePollVote(ctx context.Context, poll *gtsmodel.Poll, vote
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert votes to AS PollOptionable implementing type.
|
// Convert vote to AS Create with vote choices as Objects.
|
||||||
notes, err := f.converter.PollVoteToASOptions(ctx, vote)
|
create, err := f.converter.PollVoteToASCreate(ctx, vote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("error converting to notes: %w", err)
|
return gtserror.Newf("error converting to notes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a Create activity with PollOptionables via the Actor's outbox.
|
// Send the Create via the Actor's outbox.
|
||||||
create := typeutils.WrapPollOptionablesInCreate(notes...)
|
|
||||||
if _, err := f.FederatingActor().Send(ctx, outboxIRI, create); err != nil {
|
if _, err := f.FederatingActor().Send(ctx, outboxIRI, create); err != nil {
|
||||||
return gtserror.Newf("error sending Create activity via outbox %s: %w", outboxIRI, err)
|
return gtserror.Newf("error sending Create activity via outbox %s: %w", outboxIRI, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -480,6 +480,7 @@ type TypeUtilsTestSuite struct {
|
||||||
testEmojis map[string]*gtsmodel.Emoji
|
testEmojis map[string]*gtsmodel.Emoji
|
||||||
testReports map[string]*gtsmodel.Report
|
testReports map[string]*gtsmodel.Report
|
||||||
testMentions map[string]*gtsmodel.Mention
|
testMentions map[string]*gtsmodel.Mention
|
||||||
|
testPollVotes map[string]*gtsmodel.PollVote
|
||||||
|
|
||||||
typeconverter *typeutils.Converter
|
typeconverter *typeutils.Converter
|
||||||
}
|
}
|
||||||
|
@ -502,6 +503,7 @@ func (suite *TypeUtilsTestSuite) SetupTest() {
|
||||||
suite.testEmojis = testrig.NewTestEmojis()
|
suite.testEmojis = testrig.NewTestEmojis()
|
||||||
suite.testReports = testrig.NewTestReports()
|
suite.testReports = testrig.NewTestReports()
|
||||||
suite.testMentions = testrig.NewTestMentions()
|
suite.testMentions = testrig.NewTestMentions()
|
||||||
|
suite.testPollVotes = testrig.NewTestPollVotes()
|
||||||
suite.typeconverter = typeutils.NewConverter(&suite.state)
|
suite.typeconverter = typeutils.NewConverter(&suite.state)
|
||||||
|
|
||||||
testrig.StandardDBSetup(suite.db, nil)
|
testrig.StandardDBSetup(suite.db, nil)
|
||||||
|
|
|
@ -1659,7 +1659,17 @@ func (c *Converter) ReportToASFlag(ctx context.Context, r *gtsmodel.Report) (voc
|
||||||
return flag, nil
|
return flag, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Converter) PollVoteToASOptions(ctx context.Context, vote *gtsmodel.PollVote) ([]ap.PollOptionable, error) {
|
// PollVoteToASCreate converts a vote on a poll into a Create
|
||||||
|
// activity, suitable for federation, with each choice in the
|
||||||
|
// vote appended as a Note to the Create's Object field.
|
||||||
|
func (c *Converter) PollVoteToASCreate(
|
||||||
|
ctx context.Context,
|
||||||
|
vote *gtsmodel.PollVote,
|
||||||
|
) (vocab.ActivityStreamsCreate, error) {
|
||||||
|
if len(vote.Choices) == 0 {
|
||||||
|
panic("no vote.Choices")
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the vote is fully populated (this fetches author).
|
// Ensure the vote is fully populated (this fetches author).
|
||||||
if err := c.state.DB.PopulatePollVote(ctx, vote); err != nil {
|
if err := c.state.DB.PopulatePollVote(ctx, vote); err != nil {
|
||||||
return nil, gtserror.Newf("error populating vote from db: %w", err)
|
return nil, gtserror.Newf("error populating vote from db: %w", err)
|
||||||
|
@ -1694,11 +1704,22 @@ func (c *Converter) PollVoteToASOptions(ctx context.Context, vote *gtsmodel.Poll
|
||||||
return nil, gtserror.Newf("invalid account uri: %w", err)
|
return nil, gtserror.Newf("invalid account uri: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preallocate the return slice of notes.
|
// Allocate Create activity and address 'To' poll author.
|
||||||
notes := make([]ap.PollOptionable, len(vote.Choices))
|
create := streams.NewActivityStreamsCreate()
|
||||||
|
ap.AppendTo(create, pollAuthorIRI)
|
||||||
|
|
||||||
for i, choice := range vote.Choices {
|
// Create ID formatted as: {$voterIRI}/activity#vote/{$statusIRI}.
|
||||||
// Create new note to represent vote.
|
id := author.URI + "/activity#vote/" + poll.Status.URI
|
||||||
|
ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(create), id)
|
||||||
|
|
||||||
|
// Set Create actor appropriately.
|
||||||
|
ap.AppendActor(create, authorIRI)
|
||||||
|
|
||||||
|
// Set publish time for activity.
|
||||||
|
ap.SetPublished(create, vote.CreatedAt)
|
||||||
|
|
||||||
|
// Parse each choice to a Note and add it to the Create.
|
||||||
|
for _, choice := range vote.Choices {
|
||||||
note := streams.NewActivityStreamsNote()
|
note := streams.NewActivityStreamsNote()
|
||||||
|
|
||||||
// For AP IRI generate from author URI + poll ID + vote choice.
|
// For AP IRI generate from author URI + poll ID + vote choice.
|
||||||
|
@ -1715,9 +1736,9 @@ func (c *Converter) PollVoteToASOptions(ctx context.Context, vote *gtsmodel.Poll
|
||||||
ap.AppendInReplyTo(note, statusIRI)
|
ap.AppendInReplyTo(note, statusIRI)
|
||||||
ap.AppendTo(note, pollAuthorIRI)
|
ap.AppendTo(note, pollAuthorIRI)
|
||||||
|
|
||||||
// Set note in return slice.
|
// Append this note as Create Object.
|
||||||
notes[i] = note
|
appendStatusableToActivity(create, note, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return notes, nil
|
return create, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -879,6 +879,48 @@ func (suite *InternalToASTestSuite) TestPinnedStatusesToASOneItem() {
|
||||||
}`, string(bytes))
|
}`, string(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *InternalToASTestSuite) TestPollVoteToASCreate() {
|
||||||
|
vote := suite.testPollVotes["remote_account_1_status_2_poll_vote_local_account_1"]
|
||||||
|
|
||||||
|
create, err := suite.typeconverter.PollVoteToASCreate(context.Background(), vote)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
createI, err := ap.Serialize(create)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
bytes, err := json.MarshalIndent(createI, "", " ")
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.Equal(`{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||||
|
"id": "http://localhost:8080/users/the_mighty_zork/activity#vote/http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6",
|
||||||
|
"object": [
|
||||||
|
{
|
||||||
|
"attributedTo": "http://localhost:8080/users/the_mighty_zork",
|
||||||
|
"id": "http://localhost:8080/users/the_mighty_zork#01HEN2R65468ZG657C4ZPHJ4EX/votes/1",
|
||||||
|
"inReplyTo": "http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6",
|
||||||
|
"name": "tissues",
|
||||||
|
"to": "http://fossbros-anonymous.io/users/foss_satan",
|
||||||
|
"type": "Note"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributedTo": "http://localhost:8080/users/the_mighty_zork",
|
||||||
|
"id": "http://localhost:8080/users/the_mighty_zork#01HEN2R65468ZG657C4ZPHJ4EX/votes/2",
|
||||||
|
"inReplyTo": "http://fossbros-anonymous.io/users/foss_satan/statuses/01HEN2QRFA8H3C6QPN7RD4KSR6",
|
||||||
|
"name": "financial times",
|
||||||
|
"to": "http://fossbros-anonymous.io/users/foss_satan",
|
||||||
|
"type": "Note"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"published": "2021-09-11T11:45:37+02:00",
|
||||||
|
"to": "http://fossbros-anonymous.io/users/foss_satan",
|
||||||
|
"type": "Create"
|
||||||
|
}`, string(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
func TestInternalToASTestSuite(t *testing.T) {
|
func TestInternalToASTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(InternalToASTestSuite))
|
suite.Run(t, new(InternalToASTestSuite))
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/superseriousbusiness/activity/pub"
|
"github.com/superseriousbusiness/activity/pub"
|
||||||
"github.com/superseriousbusiness/activity/streams"
|
"github.com/superseriousbusiness/activity/streams"
|
||||||
|
@ -91,43 +90,6 @@ func WrapStatusableInCreate(status ap.Statusable, iriOnly bool) vocab.ActivitySt
|
||||||
return create
|
return create
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
func WrapStatusableInUpdate(status ap.Statusable, iriOnly bool) vocab.ActivityStreamsUpdate {
|
||||||
update := streams.NewActivityStreamsUpdate()
|
update := streams.NewActivityStreamsUpdate()
|
||||||
wrapStatusableInActivity(update, status, iriOnly)
|
wrapStatusableInActivity(update, status, iriOnly)
|
||||||
|
|
Loading…
Reference in a new issue